mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-11-02 04:11:22 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef15bc7d67 | ||
|
|
dddcb8ad8f | ||
|
|
5ea018e7d6 | ||
|
|
ac39fd2400 | ||
|
|
ce8c512642 | ||
|
|
81efce7fe7 | ||
|
|
b456b82ee4 | ||
|
|
11596dcd10 | ||
|
|
c2279cbe6f | ||
|
|
3a024cff6d | ||
|
|
4a0b2fa0ef | ||
|
|
36179ab1d2 | ||
|
|
20ddb2b8b4 | ||
|
|
b0a25f2794 | ||
|
|
ea0aebaa2e | ||
|
|
26c68aa528 | ||
|
|
4101aebeea | ||
|
|
90ac06deb6 | ||
|
|
32d66d48eb | ||
|
|
d933d455ec | ||
|
|
c421afba65 | ||
|
|
6139dacef9 | ||
|
|
affaa90d2a | ||
|
|
9f969f5fe1 | ||
|
|
fb531b2473 | ||
|
|
fe4029b3b8 | ||
|
|
7cd9a8e5dd | ||
|
|
5ba02d5b46 | ||
|
|
11834512db | ||
|
|
eeb731c07e | ||
|
|
f0fced262a | ||
|
|
8dc4465d05 | ||
|
|
d0a3eaf601 | ||
|
|
6f45f13952 | ||
|
|
328129e5bd | ||
|
|
2d05a17dfb | ||
|
|
5a36229e38 | ||
|
|
eeadeb282e | ||
|
|
3a22aa0af3 | ||
|
|
9467a0e2bb | ||
|
|
8b55dff7a5 | ||
|
|
a19b74154a | ||
|
|
0607dc4611 | ||
|
|
a3ee814539 | ||
|
|
b65140a892 | ||
|
|
77a63608d1 | ||
|
|
c6179ca9d5 | ||
|
|
f087567853 | ||
|
|
bdef6b3f5d | ||
|
|
677d941bb6 | ||
|
|
a43acc33bd | ||
|
|
326fd55e4f | ||
|
|
6037f0210f | ||
|
|
d9328027f9 | ||
|
|
7170e04aa6 | ||
|
|
20581e61c0 | ||
|
|
4af07942ed |
No files matched your search
@@ -1,6 +1,9 @@
|
||||
blank_issues_enabled: false
|
||||
version: 2.1
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://github.com/langchain-ai/docs/issues/new?template=langgraph.yml
|
||||
about: Report an issue related to the LangGraph documentation
|
||||
- name: LangChain Forum
|
||||
url: https://forum.langchain.com/
|
||||
about: General community discussions, support, and feature requests
|
||||
about: General community discussions and support
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Documentation
|
||||
description: Report an issue related to the LangGraph documentation.
|
||||
title: "DOC: <Please write a comprehensive title after the 'DOC: ' prefix>"
|
||||
labels: [documentation]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Issue with current documentation:"
|
||||
description: >
|
||||
Please make sure to leave a reference to the document/code you're
|
||||
referring to.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Idea or request for content:"
|
||||
description: >
|
||||
Please describe as clearly as possible what topics you think are missing
|
||||
from the current documentation.
|
||||
@@ -14,6 +14,19 @@ jobs:
|
||||
python-version:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
example:
|
||||
- name: A
|
||||
workdir: libs/cli/examples
|
||||
tag: langgraph-test-a
|
||||
- name: B
|
||||
workdir: libs/cli/examples/graphs
|
||||
tag: langgraph-test-b
|
||||
- name: C
|
||||
workdir: libs/cli/examples/graphs_reqs_a
|
||||
tag: langgraph-test-c
|
||||
- name: D
|
||||
workdir: libs/cli/examples/graphs_reqs_b
|
||||
tag: langgraph-test-d
|
||||
name: "CLI integration test"
|
||||
defaults:
|
||||
run:
|
||||
@@ -33,54 +46,24 @@ jobs:
|
||||
enable-cache: true
|
||||
cache-suffix: "cli-integration-test"
|
||||
ignore-nothing-to-cache: true
|
||||
- name: Setup env
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples
|
||||
run: cat .env.example > .env
|
||||
- name: Install cli globally
|
||||
if: steps.changed-files.outputs.all
|
||||
run: pip install -e .
|
||||
- name: Build and test service A
|
||||
- name: Build and test service ${{ matrix.example.name }}
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples
|
||||
working-directory: ${{ matrix.example.workdir }}
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
# The build-arg isn't used; just testing that we accept other args
|
||||
langgraph build -t langgraph-test-a
|
||||
cp .env.example .env
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -c langgraph.json -t langgraph-test-a
|
||||
- name: Build and test service B
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graphs
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
langgraph build -t langgraph-test-b
|
||||
cp ../.env.example .env
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-b
|
||||
- name: Build and test service C
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graphs_reqs_a
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
langgraph build -t langgraph-test-c
|
||||
cp ../.env.example .env
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-c
|
||||
- name: Build and test service D
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graphs_reqs_b
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
langgraph build -t langgraph-test-d
|
||||
cp ../.env.example .env
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-d
|
||||
# Build the image for this example
|
||||
langgraph build -t ${{ matrix.example.tag }}
|
||||
# Prepare environment file from local or parent example directory
|
||||
if [ -f .env.example ]; then cp .env.example .env; elif [ -f ../.env.example ]; then cp ../.env.example .env && cp ../.env.example ../.env; fi
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; if [ -f ../.env ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> ../.env; fi; fi
|
||||
# Run the integration test using the built tag
|
||||
# Compute repo root to reference the shared script robustly
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
|
||||
|
||||
- name: Build JS service
|
||||
if: steps.changed-files.outputs.all
|
||||
@@ -111,3 +94,17 @@ jobs:
|
||||
cp ../.env.example .env
|
||||
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
|
||||
LANGGRAPH_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langgraph'); print(v);")
|
||||
if [ "$LANGGRAPH_VERSION" != "1.0.0a2" ]; then
|
||||
exit 1
|
||||
fi
|
||||
LANGCHAIN_OPENAI_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langchain-openai'); print(v);")
|
||||
if [ "$LANGCHAIN_OPENAI_VERSION" != "0.3.0" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build and test prerelease reqs fail service
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graph_prerelease_reqs_fail
|
||||
run: |
|
||||
langgraph build -t langgraph-test-i || [ $? -eq 1 ]
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
echo EOF
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Annotation
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const file = JSON.parse(`${{ steps.files.outputs.added_modified_renamed }}`)[0]
|
||||
|
||||
@@ -33,8 +33,8 @@ jobs:
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "chore[deps]: upgrade dependencies with `uv lock --upgrade`"
|
||||
title: "chore[deps]: upgrade dependencies with `uv lock --upgrade`"
|
||||
commit-message: "chore(deps): upgrade dependencies with `uv lock --upgrade`"
|
||||
title: "chore(deps): upgrade dependencies with `uv lock --upgrade`"
|
||||
body: |
|
||||
This PR updates the dependencies in all Python packages using `uv lock --upgrade`.
|
||||
|
||||
|
||||
+113
-10
@@ -1,24 +1,126 @@
|
||||
# Setup
|
||||
# LangGraph Documentation
|
||||
|
||||
To setup requirements for building docs you can run:
|
||||
For more information on contributing to our documentation, see the [Contributing Guide](../CONTRIBUTING.md).
|
||||
|
||||
```bash
|
||||
uv sync --group test
|
||||
## Structure
|
||||
|
||||
The primary documentation is located in the `docs/` directory. This directory contains both the source files for the main documentation as well as the API reference doc build process.
|
||||
|
||||
### Main Documentation
|
||||
|
||||
Main documentation files are located in `docs/docs/` and are written in Markdown format. The site uses [**MkDocs**](https://www.mkdocs.org/) with the [Material theme](https://squidfunk.github.io/mkdocs-material/) and includes:
|
||||
|
||||
- **Concepts**: Core LangGraph concepts and explanations
|
||||
- **Tutorials**: Step-by-step learning guides
|
||||
- **How-tos**: Task-focused guides for specific use cases
|
||||
- **Examples**: Real-world applications and use cases
|
||||
- **Jupyter Notebooks**: Interactive tutorials that are automatically converted to markdown
|
||||
|
||||
### API Reference
|
||||
|
||||
API reference documentation is defined in `docs/docs/reference/`. Each `.md` file outlines the "template" that each page is built from. Reference content is automatically generated from docstrings in the codebase using the **mkdocstrings** plugin. Once generated, the content is plugged into the corresponding markdown file where it is referenced by using manual directives to specify which classes and/or functions are documented:
|
||||
|
||||
```markdown
|
||||
::: langgraph.graph.state.StateGraph
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
show_root_heading: true
|
||||
show_root_full_path: false
|
||||
members:
|
||||
- add_node
|
||||
- add_edge
|
||||
- add_conditional_edges
|
||||
- add_sequence
|
||||
- compile
|
||||
```
|
||||
|
||||
## Serving documentation locally
|
||||
## Build Process
|
||||
|
||||
To run the documentation server locally you can run:
|
||||
Docs are built following these steps:
|
||||
|
||||
1. **Content Processing:**
|
||||
- `_scripts/notebook_hooks.py` - Main processing pipeline that:
|
||||
- Converts how-tos/tutorial Jupyter notebooks to markdown using `notebook_convert.py`
|
||||
- Adds automatic API reference links to code blocks using `generate_api_reference_links.py`
|
||||
- Handles conditional rendering for Python/JS versions
|
||||
- Processes highlight comments and custom syntax
|
||||
|
||||
2. **API Reference Generation:**
|
||||
- **mkdocstrings** plugin extracts docstrings from Python source code
|
||||
- Manual `::: module.Class` directives in reference pages (`/docs/docs/*`) specify what to document
|
||||
- Cross-references are automatically generated between docs and API
|
||||
|
||||
3. **Site Generation:**
|
||||
- **MkDocs** processes all markdown files and generates static HTML
|
||||
- Custom hooks handle redirects and inject additional functionality
|
||||
|
||||
4. **Deployment:**
|
||||
- Site is deployed with Vercel
|
||||
- `make build-docs` generates production build (also usable for local testing)
|
||||
- Automatic redirects handle URL changes between versions
|
||||
|
||||
### Local Development
|
||||
|
||||
For local development, use the Makefile targets:
|
||||
|
||||
```bash
|
||||
# Serve docs locally with hot reloading
|
||||
make serve-docs
|
||||
|
||||
# Clean build for production testing
|
||||
make build-docs
|
||||
|
||||
# Serve with clean build
|
||||
make serve-clean-docs
|
||||
```
|
||||
|
||||
This will start the documentation server on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/).
|
||||
The `serve-docs` command:
|
||||
|
||||
- Watches source files for changes
|
||||
- Includes dirty builds for faster iteration
|
||||
- Serves on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/)
|
||||
|
||||
## Standards
|
||||
|
||||
**Docstring Format:**
|
||||
The API reference uses **Google-style docstrings** with Markdown markup. The `mkdocstrings` plugin processes these to generate documentation.
|
||||
|
||||
**Required format:**
|
||||
|
||||
```python
|
||||
def example_function(param1: str, param2: int = 5) -> bool:
|
||||
"""Brief description of the function.
|
||||
|
||||
Longer description can go here. Use Markdown syntax for
|
||||
rich formatting like **bold** and *italic*.
|
||||
|
||||
Args:
|
||||
param1: Description of the first parameter.
|
||||
param2: Description of the second parameter with default value.
|
||||
|
||||
Returns:
|
||||
Description of the return value.
|
||||
|
||||
Raises:
|
||||
ValueError: When param1 is empty.
|
||||
TypeError: When param2 is not an integer.
|
||||
|
||||
!!! warning
|
||||
This function is experimental and may change.
|
||||
|
||||
!!! version-added "Added in version 0.2.0"
|
||||
"""
|
||||
```
|
||||
|
||||
**Special Markers:**
|
||||
|
||||
- **MkDocs admonitions**: `!!! warning`, `!!! note`, `!!! version-added`
|
||||
- **Code blocks**: Standard markdown ``` syntax
|
||||
- **Cross-references**: Automatic linking via `generate_api_reference_links.py`
|
||||
|
||||
## Execute notebooks
|
||||
|
||||
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GHA, you can run:
|
||||
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GitHub action, you can run:
|
||||
|
||||
```bash
|
||||
python _scripts/prepare_notebooks_for_ci.py
|
||||
@@ -33,8 +135,9 @@ python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
|
||||
```
|
||||
|
||||
`prepare_notebooks_for_ci.py` script will add VCR cassette context manager for each cell in the notebook, so that:
|
||||
* when the notebook is run for the first time, cells with network requests will be recorded to a VCR cassette file
|
||||
* when the notebook is run subsequently, the cells with network requests will be replayed from the cassettes
|
||||
|
||||
- when the notebook is run for the first time, cells with network requests will be recorded to a VCR cassette file
|
||||
- when the notebook is run subsequently, the cells with network requests will be replayed from the cassettes
|
||||
|
||||
## Adding new notebooks
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Generate API reference links for imports in Python code blocks within markdown files."""
|
||||
|
||||
import ast
|
||||
import importlib
|
||||
import logging
|
||||
@@ -70,8 +72,18 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
|
||||
([], "langgraph.checkpoint.postgres.aio", "AsyncPostgresSaver", "checkpoints"),
|
||||
([], "langgraph.checkpoint.postgres", "PostgresSaver", "checkpoints"),
|
||||
# other prebuilts
|
||||
(["langgraph_supervisor"], "langgraph_supervisor.supervisor", "create_supervisor", "supervisor"),
|
||||
(["langgraph_supervisor"], "langgraph_supervisor.handoff", "create_handoff_tool", "supervisor"),
|
||||
(
|
||||
["langgraph_supervisor"],
|
||||
"langgraph_supervisor.supervisor",
|
||||
"create_supervisor",
|
||||
"supervisor",
|
||||
),
|
||||
(
|
||||
["langgraph_supervisor"],
|
||||
"langgraph_supervisor.handoff",
|
||||
"create_handoff_tool",
|
||||
"supervisor",
|
||||
),
|
||||
([], "langgraph_supervisor.handoff", "create_forward_message_tool", "supervisor"),
|
||||
(["langgraph_swarm"], "langgraph_swarm.swarm", "create_swarm", "swarm"),
|
||||
(["langgraph_swarm"], "langgraph_swarm.swarm", "add_active_agent_router", "swarm"),
|
||||
|
||||
Binary file not shown.
@@ -2108,9 +2108,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"hono@npm:^4.5.4":
|
||||
version: 4.9.6
|
||||
resolution: "hono@npm:4.9.6"
|
||||
checksum: 10c0/182a144eb3b9e05bd9e43d15af15c93f60d3d747fef6c6904b9993e9db8129ea7fadf6190331d6f76b1bf6dd2b2c3b13efea105236f541ef411397e30475422d
|
||||
version: 4.9.7
|
||||
resolution: "hono@npm:4.9.7"
|
||||
checksum: 10c0/089184660a9211ea216ab95bafa45260e371651cb019db49828064b7982b0ae61cc3c4715324bfeb9037aa2460c39ffa2c91d84ad0c8d500fa77cbcc7fc07a8f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Convert Jupyter notebooks to markdown with custom processing."""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import re
|
||||
|
||||
@@ -144,9 +144,9 @@ REDIRECT_MAP = {
|
||||
"concepts/langgraph_cli.md": "https://docs.langchain.com/langgraph-platform/langgraph-cli",
|
||||
"concepts/langgraph_studio.md": "https://docs.langchain.com/langgraph-platform/langgraph-studio",
|
||||
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langgraph-platform/quick-start-studio",
|
||||
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langgraph-platform/invoke-studio",
|
||||
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langgraph-platform/manage-assistants-studio",
|
||||
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langgraph-platform/threads-studio",
|
||||
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langgraph-platform/use-studio#run-application",
|
||||
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langgraph-platform/use-studio#manage-assistants",
|
||||
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langgraph-platform/use-studio#manage-threads",
|
||||
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langgraph-platform/iterate-graph-studio",
|
||||
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langgraph-platform/run-evals-studio",
|
||||
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langgraph-platform/clone-traces-studio",
|
||||
|
||||
@@ -291,7 +291,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
}
|
||||
|
||||
.md-banner {
|
||||
background-color: #CFC9FA;
|
||||
background-color: #FFAE42;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
@@ -360,5 +360,5 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
{% endblock %}
|
||||
|
||||
{% block announce %}
|
||||
Our new LangChain Academy Course Deep Research with LangGraph is now live! <a href="https://academy.langchain.com/courses/deep-research-with-langgraph/?utm_medium=internal&utm_source=docs&utm_campaign=q3-2025_deep-research-course_co" target="_blank">Enroll for free</a>.
|
||||
These docs will be deprecated and removed with the release of LangGraph v1.0 in October 2025. <a href="https://docs.langchain.com/oss/python/langgraph/overview" target="_blank">Visit the v1.0 alpha docs</a>
|
||||
{% endblock %}
|
||||
+4
-4
@@ -7,14 +7,14 @@ name = "langgraph-docs"
|
||||
version = "0.0.1"
|
||||
description = "LangGraph docs"
|
||||
authors = []
|
||||
requires-python = "~=3.11"
|
||||
requires-python = ">=3.11.0,<4.0.0"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
dependencies = [
|
||||
"aiohappyeyeballs==2.4.3",
|
||||
"hub>=3.0.1,<4",
|
||||
"xxhash>=3.5.0,<4",
|
||||
"black>=25.1.0,<26",
|
||||
"hub>=3.0.1,<4.0.0",
|
||||
"xxhash>=3.5.0,<4.0.0",
|
||||
"black>=25.1.0,<26.0.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
Generated
+5
-4
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.11, <4"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13' and platform_python_implementation != 'PyPy'",
|
||||
@@ -2337,7 +2337,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.6.2"
|
||||
version = "0.6.7"
|
||||
source = { editable = "../libs/langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -2380,6 +2380,7 @@ dev = [
|
||||
{ name = "pytest-repeat" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "pytest-xdist", extras = ["psutil"] },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
{ name = "syrupy" },
|
||||
{ name = "types-requests" },
|
||||
@@ -2413,6 +2414,7 @@ dev = [
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-watcher" },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
@@ -2643,7 +2645,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.6.2"
|
||||
version = "0.6.4"
|
||||
source = { editable = "../libs/prebuilt" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -2674,7 +2676,6 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.2.0"
|
||||
source = { editable = "../libs/sdk-py" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"id": "18526f23",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/persistence_postgres.ipynb"
|
||||
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/memory/add-memory.md"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,11 +7,6 @@ from contextlib import contextmanager
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
ChannelVersions,
|
||||
@@ -21,10 +16,15 @@ from langgraph.checkpoint.base import (
|
||||
get_checkpoint_id,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import _internal
|
||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||
from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
|
||||
Conn = _internal.Conn # For backward compatibility
|
||||
|
||||
@@ -450,7 +450,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
{
|
||||
**value["checkpoint"],
|
||||
"channel_values": {
|
||||
**value["checkpoint"].get("channel_values"),
|
||||
**(value["checkpoint"].get("channel_values") or {}),
|
||||
**self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,11 +7,6 @@ from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
ChannelVersions,
|
||||
@@ -21,10 +16,15 @@ from langgraph.checkpoint.base import (
|
||||
get_checkpoint_id,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import _ainternal
|
||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||
from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
|
||||
Conn = _ainternal.Conn # For backward compatibility
|
||||
|
||||
@@ -409,7 +409,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
{
|
||||
**value["checkpoint"],
|
||||
"channel_values": {
|
||||
**value["checkpoint"].get("channel_values"),
|
||||
**(value["checkpoint"].get("channel_values") or {}),
|
||||
**self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,8 +5,6 @@ from collections.abc import Sequence
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
@@ -14,6 +12,7 @@ from langgraph.checkpoint.base import (
|
||||
get_checkpoint_id,
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
MetadataInput = Optional[dict[str, Any]]
|
||||
|
||||
|
||||
@@ -6,6 +6,16 @@ from contextlib import asynccontextmanager, contextmanager
|
||||
from typing import Any, Optional
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
from psycopg import (
|
||||
AsyncConnection,
|
||||
AsyncCursor,
|
||||
@@ -19,18 +29,8 @@ from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.postgres import _ainternal, _internal
|
||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
|
||||
"""
|
||||
To add a new migration, add a new string to the MIGRATIONS list.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from langgraph.store.postgres.aio import AsyncPostgresStore
|
||||
from langgraph.store.postgres.base import PostgresStore
|
||||
from langgraph.store.postgres.base import PoolConfig, PostgresStore
|
||||
|
||||
__all__ = ["AsyncPostgresStore", "PostgresStore"]
|
||||
__all__ = ["AsyncPostgresStore", "PoolConfig", "PostgresStore"]
|
||||
@@ -8,11 +8,6 @@ from types import TracebackType
|
||||
from typing import Any, Callable, cast
|
||||
|
||||
import orjson
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import _ainternal
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
ListNamespacesOp,
|
||||
@@ -22,6 +17,11 @@ from langgraph.store.base import (
|
||||
SearchOp,
|
||||
)
|
||||
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import _ainternal
|
||||
from langgraph.store.postgres.base import (
|
||||
PLACEHOLDER,
|
||||
BasePostgresStore,
|
||||
|
||||
@@ -22,14 +22,6 @@ from typing import (
|
||||
)
|
||||
|
||||
import orjson
|
||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import ConnectionPool
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.postgres import _ainternal as _ainternal
|
||||
from langgraph.checkpoint.postgres import _internal as _pg_internal
|
||||
from langgraph.store.base import (
|
||||
BaseStore,
|
||||
GetOp,
|
||||
@@ -46,6 +38,14 @@ from langgraph.store.base import (
|
||||
get_text_at_path,
|
||||
tokenize_path,
|
||||
)
|
||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import ConnectionPool
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.postgres import _ainternal as _ainternal
|
||||
from langgraph.checkpoint.postgres import _internal as _pg_internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.embeddings import Embeddings
|
||||
|
||||
@@ -6,10 +6,6 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import AsyncConnection
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
EXCLUDED_METADATA_KEYS,
|
||||
Checkpoint,
|
||||
@@ -17,11 +13,15 @@ from langgraph.checkpoint.base import (
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
from psycopg import AsyncConnection
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres.aio import (
|
||||
AsyncPostgresSaver,
|
||||
AsyncShallowPostgresSaver,
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
from tests.conftest import DEFAULT_POSTGRES_URI
|
||||
|
||||
|
||||
@@ -344,3 +344,34 @@ async def test_pending_sends_migration(saver_name: str) -> None:
|
||||
TASKS: ["send-1", "send-2", "send-3"]
|
||||
}
|
||||
assert TASKS in search_results[0].checkpoint["channel_versions"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
|
||||
async def test_get_checkpoint_no_channel_values(
|
||||
monkeypatch, saver_name: str, test_data
|
||||
) -> None:
|
||||
"""Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
|
||||
async with _saver(saver_name) as saver:
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "",
|
||||
"__super_private_key": "super_private_value",
|
||||
},
|
||||
"metadata": {"run_id": "my_run_id"},
|
||||
}
|
||||
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
|
||||
await saver.aput(config, chkpnt, {}, {})
|
||||
|
||||
load_checkpoint_tuple = saver._load_checkpoint_tuple
|
||||
|
||||
def patched_load_checkpoint_tuple(value):
|
||||
value["checkpoint"].pop("channel_values", None)
|
||||
return load_checkpoint_tuple(value)
|
||||
|
||||
monkeypatch.setattr(
|
||||
saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
|
||||
)
|
||||
|
||||
checkpoint = await saver.aget_tuple(config)
|
||||
assert checkpoint.checkpoint["channel_values"] == {}
|
||||
@@ -12,8 +12,6 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
from langchain_core.embeddings import Embeddings
|
||||
from psycopg import AsyncConnection
|
||||
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
Item,
|
||||
@@ -21,6 +19,8 @@ from langgraph.store.base import (
|
||||
PutOp,
|
||||
SearchOp,
|
||||
)
|
||||
from psycopg import AsyncConnection
|
||||
|
||||
from langgraph.store.postgres import AsyncPostgresStore
|
||||
from tests.conftest import (
|
||||
DEFAULT_URI,
|
||||
|
||||
@@ -9,8 +9,6 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from langchain_core.embeddings import Embeddings
|
||||
from psycopg import Connection
|
||||
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
Item,
|
||||
@@ -19,6 +17,8 @@ from langgraph.store.base import (
|
||||
PutOp,
|
||||
SearchOp,
|
||||
)
|
||||
from psycopg import Connection
|
||||
|
||||
from langgraph.store.postgres import PostgresStore
|
||||
from tests.conftest import (
|
||||
DEFAULT_URI,
|
||||
@@ -879,12 +879,7 @@ def test_non_ascii(
|
||||
distance_type: str,
|
||||
) -> None:
|
||||
"""Test support for non-ascii characters"""
|
||||
with _create_vector_store(
|
||||
vector_type,
|
||||
distance_type,
|
||||
fake_embeddings
|
||||
) as store:
|
||||
|
||||
with _create_vector_store(vector_type, distance_type, fake_embeddings) as store:
|
||||
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
|
||||
store.put(
|
||||
("user_123", "memories"), "2", {"text": "これは日本語です"}
|
||||
|
||||
@@ -7,10 +7,6 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import Connection
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
EXCLUDED_METADATA_KEYS,
|
||||
Checkpoint,
|
||||
@@ -18,8 +14,12 @@ from langgraph.checkpoint.base import (
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
from psycopg import Connection
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
|
||||
from tests.conftest import DEFAULT_POSTGRES_URI
|
||||
|
||||
|
||||
@@ -332,3 +332,33 @@ def test_pending_sends_migration(saver_name: str) -> None:
|
||||
TASKS: ["send-1", "send-2", "send-3"]
|
||||
}
|
||||
assert TASKS in search_results[0].checkpoint["channel_versions"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
|
||||
def test_get_checkpoint_no_channel_values(
|
||||
monkeypatch, saver_name: str, test_data
|
||||
) -> None:
|
||||
"""Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
|
||||
with _saver(saver_name) as saver:
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "",
|
||||
"__super_private_key": "super_private_value",
|
||||
},
|
||||
}
|
||||
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
|
||||
saver.put(config, chkpnt, {}, {})
|
||||
|
||||
load_checkpoint_tuple = saver._load_checkpoint_tuple
|
||||
|
||||
def patched_load_checkpoint_tuple(value):
|
||||
value["checkpoint"].pop("channel_values", None)
|
||||
return load_checkpoint_tuple(value)
|
||||
|
||||
monkeypatch.setattr(
|
||||
saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
|
||||
)
|
||||
|
||||
checkpoint = saver.get_tuple(config)
|
||||
assert checkpoint.checkpoint["channel_values"] == {}
|
||||
Generated
+482
-497
File diff suppressed because it is too large.
Load diff
@@ -8,7 +8,6 @@ from contextlib import closing, contextmanager
|
||||
from typing import Any, cast
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
@@ -21,6 +20,7 @@ from langgraph.checkpoint.base import (
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
|
||||
from langgraph.checkpoint.sqlite.utils import search_where
|
||||
|
||||
_AIO_ERROR_MSG = (
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Any, Callable, TypeVar, cast
|
||||
|
||||
import aiosqlite
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
@@ -21,6 +20,7 @@ from langgraph.checkpoint.base import (
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
|
||||
from langgraph.checkpoint.sqlite.utils import search_where
|
||||
|
||||
T = TypeVar("T", bound=Callable)
|
||||
|
||||
@@ -5,7 +5,6 @@ from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import get_checkpoint_id
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from typing import Any, Callable, cast
|
||||
import aiosqlite
|
||||
import orjson
|
||||
import sqlite_vec # type: ignore[import-untyped]
|
||||
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
ListNamespacesOp,
|
||||
@@ -22,6 +21,7 @@ from langgraph.store.base import (
|
||||
TTLConfig,
|
||||
)
|
||||
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
||||
|
||||
from langgraph.store.sqlite.base import (
|
||||
_PLACEHOLDER,
|
||||
BaseSqliteStore,
|
||||
@@ -507,7 +507,9 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
||||
results: List to store results in.
|
||||
cur: Database cursor.
|
||||
"""
|
||||
queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
|
||||
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
|
||||
search_ops
|
||||
)
|
||||
|
||||
# Setup dot_product function if it doesn't exist
|
||||
if embedding_requests and self.embeddings:
|
||||
@@ -515,23 +517,60 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
||||
[query for _, query in embedding_requests]
|
||||
)
|
||||
|
||||
for (idx, _), embedding in zip(embedding_requests, vectors):
|
||||
_params_list: list = queries[idx][1]
|
||||
for i, param in enumerate(_params_list):
|
||||
if param is _PLACEHOLDER:
|
||||
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
||||
for (embed_req_idx, _), embedding in zip(embedding_requests, vectors):
|
||||
# Find the corresponding query in prepared_queries
|
||||
# The embed_req_idx is the original index in search_ops, which should map to prepared_queries
|
||||
if embed_req_idx < len(prepared_queries):
|
||||
_params_list: list = prepared_queries[embed_req_idx][1]
|
||||
for i, param in enumerate(_params_list):
|
||||
if param is _PLACEHOLDER:
|
||||
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
|
||||
)
|
||||
|
||||
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||
for (original_op_idx, _), (query, params, needs_refresh) in zip(
|
||||
search_ops, prepared_queries
|
||||
):
|
||||
await cur.execute(query, params)
|
||||
rows = await cur.fetchall()
|
||||
|
||||
if "score" in query:
|
||||
if needs_refresh and rows and self.ttl_config:
|
||||
keys_to_refresh = []
|
||||
for row_data in rows:
|
||||
# Assuming row_data[0] is prefix (text), row_data[1] is key (text)
|
||||
# These are raw text values directly from the DB.
|
||||
keys_to_refresh.append((row_data[0], row_data[1]))
|
||||
|
||||
if keys_to_refresh:
|
||||
updates_by_prefix = defaultdict(list)
|
||||
for prefix_text, key_text in keys_to_refresh:
|
||||
updates_by_prefix[prefix_text].append(key_text)
|
||||
|
||||
for prefix_text, key_list in updates_by_prefix.items():
|
||||
placeholders = ",".join(["?"] * len(key_list))
|
||||
update_query = f"""
|
||||
UPDATE store
|
||||
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
||||
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
|
||||
"""
|
||||
update_params = (prefix_text, *key_list)
|
||||
try:
|
||||
await cur.execute(update_query, update_params)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error during TTL refresh update for search: {e}"
|
||||
)
|
||||
|
||||
# Process rows into items
|
||||
if "score" in query: # Vector search query
|
||||
items = [
|
||||
_row_to_search_item(
|
||||
_decode_ns_text(row[0]),
|
||||
_decode_ns_text(row[0]), # prefix
|
||||
{
|
||||
"key": row[1],
|
||||
"value": row[2],
|
||||
"key": row[1], # key
|
||||
"value": row[2], # value
|
||||
"created_at": row[3],
|
||||
"updated_at": row[4],
|
||||
"expires_at": row[5] if len(row) > 5 else None,
|
||||
@@ -545,10 +584,10 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
||||
else: # Regular search query
|
||||
items = [
|
||||
_row_to_search_item(
|
||||
_decode_ns_text(row[0]),
|
||||
_decode_ns_text(row[0]), # prefix
|
||||
{
|
||||
"key": row[1],
|
||||
"value": row[2],
|
||||
"key": row[1], # key
|
||||
"value": row[2], # value
|
||||
"created_at": row[3],
|
||||
"updated_at": row[4],
|
||||
"expires_at": row[5] if len(row) > 5 else None,
|
||||
@@ -559,7 +598,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
||||
for row in rows
|
||||
]
|
||||
|
||||
results[idx] = items
|
||||
results[original_op_idx] = items
|
||||
|
||||
async def _batch_list_namespaces_ops(
|
||||
self,
|
||||
|
||||
@@ -13,7 +13,6 @@ from typing import Any, Callable, Literal, NamedTuple, cast
|
||||
|
||||
import orjson
|
||||
import sqlite_vec # type: ignore[import-untyped]
|
||||
|
||||
from langgraph.store.base import (
|
||||
BaseStore,
|
||||
GetOp,
|
||||
@@ -372,13 +371,15 @@ class BaseSqliteStore:
|
||||
def _prepare_batch_search_queries(
|
||||
self, search_ops: Sequence[tuple[int, SearchOp]]
|
||||
) -> tuple[
|
||||
list[tuple[str, list[None | str | list[float]]]], # queries, params
|
||||
list[
|
||||
tuple[str, list[None | str | list[float]], bool]
|
||||
], # queries, params, needs_refresh
|
||||
list[tuple[int, str]], # idx, query_text pairs to embed
|
||||
]:
|
||||
"""
|
||||
Build per-SearchOp SQL queries (with optional TTL refresh) plus embedding requests.
|
||||
Build per-SearchOp SQL queries (with optional TTL refresh flag) plus embedding requests.
|
||||
Returns:
|
||||
- queries: list of (SQL, param_list)
|
||||
- queries: list of (SQL, param_list, needs_ttl_refresh_flag)
|
||||
- embedding_requests: list of (original_index_in_search_ops, text_query)
|
||||
"""
|
||||
queries = []
|
||||
@@ -519,30 +520,18 @@ class BaseSqliteStore:
|
||||
logger.debug(f"Search query: {base_query}")
|
||||
logger.debug(f"Search params: {params}")
|
||||
|
||||
# Handle TTL refresh if requested
|
||||
if (
|
||||
# Determine if TTL refresh is needed
|
||||
needs_ttl_refresh = bool(
|
||||
op.refresh_ttl
|
||||
and self.ttl_config
|
||||
and self.ttl_config.get("refresh_on_read", False)
|
||||
):
|
||||
final_sql = f"""
|
||||
WITH search_results AS (
|
||||
{base_query}
|
||||
),
|
||||
updated AS (
|
||||
UPDATE store
|
||||
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
||||
WHERE (prefix, key) IN (SELECT prefix, key FROM search_results)
|
||||
AND ttl_minutes IS NOT NULL
|
||||
)
|
||||
SELECT * FROM search_results
|
||||
"""
|
||||
final_params = params[:] # copy params
|
||||
else:
|
||||
final_sql = base_query
|
||||
final_params = params
|
||||
)
|
||||
|
||||
queries.append((final_sql, final_params))
|
||||
# The base_query is now the final_sql, and we pass the refresh flag
|
||||
final_sql = base_query
|
||||
final_params = params
|
||||
|
||||
queries.append((final_sql, final_params, needs_ttl_refresh))
|
||||
|
||||
return queries, embedding_requests
|
||||
|
||||
@@ -1331,7 +1320,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
results: list[Result],
|
||||
cur: sqlite3.Cursor,
|
||||
) -> None:
|
||||
queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
|
||||
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
|
||||
search_ops
|
||||
)
|
||||
|
||||
# Setup similarity functions if they don't exist
|
||||
if embedding_requests and self.embeddings:
|
||||
@@ -1341,16 +1332,48 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
)
|
||||
|
||||
# Replace placeholders with actual embeddings
|
||||
for (idx, _), embedding in zip(embedding_requests, embeddings):
|
||||
_params_list: list = queries[idx][1]
|
||||
for i, param in enumerate(_params_list):
|
||||
if param is _PLACEHOLDER:
|
||||
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
||||
for (embed_req_idx, _), embedding in zip(embedding_requests, embeddings):
|
||||
if embed_req_idx < len(prepared_queries):
|
||||
_params_list: list = prepared_queries[embed_req_idx][1]
|
||||
for i, param in enumerate(_params_list):
|
||||
if param is _PLACEHOLDER:
|
||||
_params_list[i] = sqlite_vec.serialize_float32(embedding)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
|
||||
)
|
||||
|
||||
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||
for (original_op_idx, _), (query, params, needs_refresh) in zip(
|
||||
search_ops, prepared_queries
|
||||
):
|
||||
cur.execute(query, params)
|
||||
rows = cur.fetchall()
|
||||
|
||||
if needs_refresh and rows and self.ttl_config:
|
||||
keys_to_refresh = []
|
||||
for row_data in rows:
|
||||
keys_to_refresh.append((row_data[0], row_data[1]))
|
||||
|
||||
if keys_to_refresh:
|
||||
updates_by_prefix = defaultdict(list)
|
||||
for prefix_text, key_text in keys_to_refresh:
|
||||
updates_by_prefix[prefix_text].append(key_text)
|
||||
|
||||
for prefix_text, key_list in updates_by_prefix.items():
|
||||
placeholders = ",".join(["?"] * len(key_list))
|
||||
update_query = f"""
|
||||
UPDATE store
|
||||
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
||||
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
|
||||
"""
|
||||
update_params = (prefix_text, *key_list)
|
||||
try:
|
||||
cur.execute(update_query, update_params)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error during TTL refresh update for search: {e}"
|
||||
)
|
||||
|
||||
if "score" in query: # Vector search query
|
||||
items = [
|
||||
_row_to_search_item(
|
||||
@@ -1385,7 +1408,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
for row in rows
|
||||
]
|
||||
|
||||
results[idx] = items
|
||||
results[original_op_idx] = items
|
||||
|
||||
def _batch_list_namespaces_ops(
|
||||
self,
|
||||
|
||||
@@ -2,13 +2,13 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from contextlib import asynccontextmanager
|
||||
from typing import Optional, Union, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
Item,
|
||||
@@ -16,6 +15,7 @@ from langgraph.store.base import (
|
||||
PutOp,
|
||||
SearchOp,
|
||||
)
|
||||
|
||||
from langgraph.store.sqlite import AsyncSqliteStore
|
||||
from langgraph.store.sqlite.base import SqliteIndexConfig
|
||||
from tests.test_store import CharacterEmbeddings
|
||||
|
||||
@@ -2,13 +2,13 @@ from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.checkpoint.sqlite.utils import _metadata_predicate, search_where
|
||||
|
||||
@@ -116,7 +116,17 @@ class TestSqliteSaver:
|
||||
search_results_5[1].config["configurable"]["checkpoint_ns"],
|
||||
} == {"", "inner"}
|
||||
|
||||
# TODO: test before and limit params
|
||||
# search with before param
|
||||
search_results_6 = list(saver.list(None, before=search_results_5[1].config))
|
||||
assert len(search_results_6) == 1
|
||||
assert search_results_6[0].config["configurable"]["thread_id"] == "thread-1"
|
||||
|
||||
# search with limit param
|
||||
search_results_7 = list(
|
||||
saver.list({"configurable": {"thread_id": "thread-2"}}, limit=1)
|
||||
)
|
||||
assert len(search_results_7) == 1
|
||||
assert search_results_7[0].config["configurable"]["thread_id"] == "thread-2"
|
||||
|
||||
def test_search_where(self) -> None:
|
||||
# call method / assertions
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import Any, Literal, Optional, Union, cast
|
||||
|
||||
import pytest
|
||||
from langchain_core.embeddings import Embeddings
|
||||
|
||||
from langgraph.store.base import (
|
||||
GetOp,
|
||||
Item,
|
||||
@@ -18,6 +17,7 @@ from langgraph.store.base import (
|
||||
PutOp,
|
||||
SearchOp,
|
||||
)
|
||||
|
||||
from langgraph.store.sqlite import SqliteStore
|
||||
from langgraph.store.sqlite.base import SqliteIndexConfig
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import time
|
||||
from collections.abc import Generator
|
||||
|
||||
import pytest
|
||||
from langgraph.store.base import TTLConfig
|
||||
|
||||
from langgraph.store.sqlite import SqliteStore
|
||||
from langgraph.store.sqlite.aio import AsyncSqliteStore
|
||||
@@ -93,9 +94,13 @@ def test_ttl_sweeper(temp_db_file: str) -> None:
|
||||
ttl_seconds = 2
|
||||
ttl_minutes = ttl_seconds / 60
|
||||
|
||||
ttl_config: TTLConfig = {
|
||||
"default_ttl": ttl_minutes,
|
||||
"sweep_interval_minutes": ttl_minutes / 2,
|
||||
}
|
||||
with SqliteStore.from_conn_string(
|
||||
temp_db_file,
|
||||
ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
|
||||
ttl=ttl_config,
|
||||
) as store:
|
||||
store.setup()
|
||||
|
||||
@@ -298,9 +303,14 @@ async def test_async_ttl_sweeper(temp_db_file: str) -> None:
|
||||
ttl_seconds = 2
|
||||
ttl_minutes = ttl_seconds / 60
|
||||
|
||||
ttl_config: TTLConfig = {
|
||||
"default_ttl": ttl_minutes,
|
||||
"sweep_interval_minutes": ttl_minutes / 2,
|
||||
}
|
||||
|
||||
async with AsyncSqliteStore.from_conn_string(
|
||||
temp_db_file,
|
||||
ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
|
||||
ttl=ttl_config,
|
||||
) as store:
|
||||
await store.setup()
|
||||
|
||||
@@ -353,3 +363,67 @@ async def test_async_search_with_ttl(temp_db_file: str) -> None:
|
||||
# Search after expiration
|
||||
results = await store.asearch(("test",), filter={"value": "apple"})
|
||||
assert len(results) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.flaky(retries=3)
|
||||
async def test_async_asearch_refresh_ttl(temp_db_file: str) -> None:
|
||||
"""Test TTL refresh on asearch with async API."""
|
||||
ttl_seconds = 4.0 # Increased TTL for less sensitivity to timing
|
||||
ttl_minutes = ttl_seconds / 60.0
|
||||
|
||||
async with AsyncSqliteStore.from_conn_string(
|
||||
temp_db_file, ttl={"default_ttl": ttl_minutes, "refresh_on_read": True}
|
||||
) as store:
|
||||
await store.setup()
|
||||
|
||||
namespace = ("docs", "user1")
|
||||
# t=0: items put, expire at t=4.0s
|
||||
await store.aput(namespace, "item1", {"text": "content1", "id": 1})
|
||||
await store.aput(namespace, "item2", {"text": "content2", "id": 2})
|
||||
|
||||
# t=3.0s: (after sleep ttl_seconds * 0.75 = 3s)
|
||||
await asyncio.sleep(ttl_seconds * 0.75)
|
||||
|
||||
# Perform asearch with refresh_ttl=True for item1.
|
||||
# item1's TTL should be refreshed. New expiry: t=3.0s + 4.0s = t=7.0s.
|
||||
# item2's TTL is not affected. Expires at t=4.0s.
|
||||
searched_items = await store.asearch(
|
||||
namespace, filter={"id": 1}, refresh_ttl=True
|
||||
)
|
||||
assert len(searched_items) == 1
|
||||
assert searched_items[0].key == "item1"
|
||||
|
||||
# t=5.0s: (after sleep ttl_seconds * 0.5 = 2s more. Total elapsed: 3s + 2s = 5s)
|
||||
await asyncio.sleep(ttl_seconds * 0.5)
|
||||
# At this point:
|
||||
# - item1 (refreshed by asearch) should expire at t=7.0s. Should be ALIVE.
|
||||
# - item2 (original TTL) should have expired at t=4.0s. Should be GONE after sweep.
|
||||
|
||||
await store.sweep_ttl()
|
||||
|
||||
# Check item1 (should exist due to asearch refresh)
|
||||
item1_check1 = await store.aget(namespace, "item1", refresh_ttl=False)
|
||||
assert item1_check1 is not None, (
|
||||
"Item1 should exist after asearch refresh and first sweep"
|
||||
)
|
||||
assert item1_check1.value["text"] == "content1"
|
||||
|
||||
# Check item2 (should be gone)
|
||||
item2_check1 = await store.aget(namespace, "item2", refresh_ttl=False)
|
||||
assert item2_check1 is None, (
|
||||
"Item2 should be gone after its original TTL expired"
|
||||
)
|
||||
|
||||
# t=7.5s: (after sleep ttl_seconds * 0.625 = 2.5s more. Total elapsed: 5s + 2.5s = 7.5s)
|
||||
await asyncio.sleep(ttl_seconds * 0.625)
|
||||
# At this point:
|
||||
# - item1 (refreshed by asearch, expired at t=7.0s) should be GONE after sweep.
|
||||
|
||||
await store.sweep_ttl()
|
||||
|
||||
# Check item1 again (should be gone now)
|
||||
item1_final_check = await store.aget(namespace, "item1", refresh_ttl=False)
|
||||
assert item1_final_check is None, (
|
||||
"Item1 should be gone after its refreshed TTL expired"
|
||||
)
|
||||
Generated
+424
-438
File diff suppressed because it is too large.
Load diff
@@ -950,8 +950,8 @@ async def test_embed_with_path(fake_embeddings: CharacterEmbeddings) -> None:
|
||||
assert results[0].key != results[1].key
|
||||
ascore = results[0].score
|
||||
bscore = results[1].score
|
||||
assert ascore == bscore
|
||||
assert ascore is not None and bscore is not None
|
||||
assert ascore == pytest.approx(bscore, abs=1e-5)
|
||||
|
||||
results = await store.asearch(("test",), query="uuu")
|
||||
assert len(results) == 2
|
||||
|
||||
Generated
+551
-542
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,6 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Annotated, Literal, TypedDict
|
||||
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langchain_community.tools.tavily_search import TavilySearchResults
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
@@ -10,10 +9,8 @@ from langgraph.prebuilt import ToolNode
|
||||
|
||||
tools = [TavilySearchResults(max_results=1)]
|
||||
|
||||
model_anth = init_chat_model("claude-3-7-sonnet-20250219", model_provider="anthropic")
|
||||
model_oai = ChatOpenAI(temperature=0)
|
||||
|
||||
model_anth = model_anth.bind_tools(tools)
|
||||
model_oai = model_oai.bind_tools(tools)
|
||||
|
||||
|
||||
@@ -35,10 +32,7 @@ def should_continue(state):
|
||||
|
||||
# Define the function that calls the model
|
||||
def call_model(state, config):
|
||||
if config["configurable"].get("model", "anthropic") == "anthropic":
|
||||
model = model_anth
|
||||
else:
|
||||
model = model_oai
|
||||
model = model_oai
|
||||
messages = state["messages"]
|
||||
response = model.invoke(messages)
|
||||
# We return a list, because this will get added to the existing list
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[project]
|
||||
name = "graph-prerelease-reqs-additional-deps"
|
||||
version = "0.1.0"
|
||||
description = "Test for prerelease stuff"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langgraph==0.6.0"
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[project]
|
||||
name = "graph-prerelease-reqs-zuper-deps"
|
||||
version = "0.1.0"
|
||||
description = "Test for prerelease stuff"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langchain-openai==0.3.0"
|
||||
]
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"python_version": "3.12",
|
||||
"dependencies": [
|
||||
"."
|
||||
".",
|
||||
"./deps/additional_deps",
|
||||
"./deps/zuper_deps"
|
||||
],
|
||||
"graphs": {
|
||||
"agent": "./agent.py:graph"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[project]
|
||||
name = "graph-prerelease-reqs"
|
||||
version = "0.1.0"
|
||||
description = "Test for prerelease stuff"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langchain-openai==1.0.0a2",
|
||||
"langgraph==1.0.0a2",
|
||||
"langchain_community>=0.3.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
prerelease = "allow"
|
||||
@@ -1,6 +0,0 @@
|
||||
requests
|
||||
langchain_anthropic
|
||||
langchain_openai
|
||||
langchain_community
|
||||
langchain
|
||||
langgraph==1.0.0a2
|
||||
@@ -0,0 +1,89 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Annotated, Literal, TypedDict
|
||||
|
||||
from langchain_community.tools.tavily_search import TavilySearchResults
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import END, StateGraph, add_messages
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
tools = [TavilySearchResults(max_results=1)]
|
||||
|
||||
model_oai = ChatOpenAI(temperature=0)
|
||||
|
||||
model_oai = model_oai.bind_tools(tools)
|
||||
|
||||
|
||||
class AgentState(TypedDict):
|
||||
messages: Annotated[Sequence[BaseMessage], add_messages]
|
||||
|
||||
|
||||
# Define the function that determines whether to continue or not
|
||||
def should_continue(state):
|
||||
messages = state["messages"]
|
||||
last_message = messages[-1]
|
||||
# If there are no tool calls, then we finish
|
||||
if not last_message.tool_calls:
|
||||
return "end"
|
||||
# Otherwise if there is, we continue
|
||||
else:
|
||||
return "continue"
|
||||
|
||||
|
||||
# Define the function that calls the model
|
||||
def call_model(state, config):
|
||||
model = model_oai
|
||||
messages = state["messages"]
|
||||
response = model.invoke(messages)
|
||||
# We return a list, because this will get added to the existing list
|
||||
return {"messages": [response]}
|
||||
|
||||
|
||||
# Define the function to execute tools
|
||||
tool_node = ToolNode(tools)
|
||||
|
||||
|
||||
class ContextSchema(TypedDict):
|
||||
model: Literal["anthropic", "openai"]
|
||||
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(AgentState, context_schema=ContextSchema)
|
||||
|
||||
# Define the two nodes we will cycle between
|
||||
workflow.add_node("agent", call_model)
|
||||
workflow.add_node("action", tool_node)
|
||||
|
||||
# Set the entrypoint as `agent`
|
||||
# This means that this node is the first one called
|
||||
workflow.set_entry_point("agent")
|
||||
|
||||
# We now add a conditional edge
|
||||
workflow.add_conditional_edges(
|
||||
# First, we define the start node. We use `agent`.
|
||||
# This means these are the edges taken after the `agent` node is called.
|
||||
"agent",
|
||||
# Next, we pass in the function that will determine which node is called next.
|
||||
should_continue,
|
||||
# Finally we pass in a mapping.
|
||||
# The keys are strings, and the values are other nodes.
|
||||
# END is a special node marking that the graph should finish.
|
||||
# What will happen is we will call `should_continue`, and then the output of that
|
||||
# will be matched against the keys in this mapping.
|
||||
# Based on which one it matches, that node will then be called.
|
||||
{
|
||||
# If `tools`, then we call the tool node.
|
||||
"continue": "action",
|
||||
# Otherwise we finish.
|
||||
"end": END,
|
||||
},
|
||||
)
|
||||
|
||||
# We now add a normal edge from `tools` to `agent`.
|
||||
# This means that after `tools` is called, `agent` node is called next.
|
||||
workflow.add_edge("action", "agent")
|
||||
|
||||
# Finally, we compile it!
|
||||
# This compiles it into a LangChain Runnable,
|
||||
# meaning you can use it as you would any other runnable
|
||||
graph = workflow.compile()
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"python_version": "3.12",
|
||||
"dependencies": [
|
||||
"."
|
||||
],
|
||||
"graphs": {
|
||||
"agent": "./agent.py:graph"
|
||||
},
|
||||
"env": "../.env"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[project]
|
||||
name = "graph-prerelease-reqs"
|
||||
version = "0.1.0"
|
||||
description = "Test for prerelease stuff"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langchain-openai==1.0.0a2",
|
||||
"langgraph==1.0.0a2",
|
||||
"langchain_community>=0.3.0",
|
||||
]
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://langgra.ph/schema.json",
|
||||
"python_version": "3.12",
|
||||
"dependencies": [
|
||||
"langchain_community",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://langgra.ph/schema.json",
|
||||
"dependencies": [
|
||||
"."
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://langgra.ph/schema.json",
|
||||
"dependencies": [
|
||||
"."
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://langgra.ph/schema.json",
|
||||
"node_version": "20",
|
||||
"graphs": {
|
||||
"agent": "./src/agent/graph.ts:graph"
|
||||
|
||||
@@ -16,8 +16,10 @@ DEFAULT_PYTHON_VERSION = "3.11"
|
||||
|
||||
DEFAULT_IMAGE_DISTRO = "debian"
|
||||
|
||||
CONSTRAINTS_PATH = "/api/constraints.txt"
|
||||
|
||||
Distros = Literal["debian", "wolfi", "bullseye", "bookworm"]
|
||||
MiddlewareOrders = Literal["auth_first", "middleware_first"]
|
||||
|
||||
|
||||
class TTLConfig(TypedDict, total=False):
|
||||
@@ -359,6 +361,25 @@ class HttpConfig(TypedDict, total=False):
|
||||
agent's behavior or permissions on a request's headers."""
|
||||
logging_headers: Optional[ConfigurableHeaderConfig]
|
||||
"""Optional. Defines which headers are excluded from logging."""
|
||||
middleware_order: Optional[MiddlewareOrders]
|
||||
"""Optional. Defines the order in which to apply server customizations.
|
||||
|
||||
Choices:
|
||||
- "auth_first": Authentication hooks (custom or default) are evaluated
|
||||
before custom middleware.
|
||||
- "middleware_first": Custom middleware is evaluated
|
||||
before authentication hooks (custom or default).
|
||||
|
||||
Default is `middleware_first`.
|
||||
"""
|
||||
enable_custom_route_auth: bool
|
||||
"""Optional. If True, authentication is enabled for custom routes,
|
||||
not just the routes that are protected by default.
|
||||
(Routes protected by default include /assistants, /threads, and /runs).
|
||||
|
||||
Default is False. This flag only affects authentication behavior
|
||||
if `app` is provided and contains custom routes.
|
||||
"""
|
||||
|
||||
|
||||
class Config(TypedDict, total=False):
|
||||
@@ -773,6 +794,15 @@ def validate_config_file(config_path: pathlib.Path) -> Config:
|
||||
return validated
|
||||
|
||||
|
||||
class ReqGenSpec(NamedTuple):
|
||||
host_pkg_path: pathlib.Path
|
||||
container_pkg_path: str
|
||||
container_req_path: str
|
||||
package_type: Literal["pyproject", "setup"]
|
||||
has_uv_lock: bool
|
||||
stage_name: Optional[str]
|
||||
|
||||
|
||||
class LocalDeps(NamedTuple):
|
||||
"""A container for referencing and managing local Python dependencies.
|
||||
|
||||
@@ -820,6 +850,15 @@ class LocalDeps(NamedTuple):
|
||||
additional_contexts: A list of paths to directories that contain local
|
||||
dependencies in parent directories. These directories are added to the
|
||||
Docker build context to ensure that the Dockerfile can access them.
|
||||
|
||||
pkgs_missing_reqs: A list of packages that need requirements.txt generated.
|
||||
Each entry is a ReqGenSpec containing:
|
||||
- host_pkg_path: Absolute host path of the package directory
|
||||
- container_pkg_path: Target path in the container for metadata/requirements
|
||||
- container_req_path: Full path to requirements.txt inside the container
|
||||
- package_type: "pyproject" or "setup"
|
||||
- has_uv_lock: True if the package includes uv.lock
|
||||
- stage_name: BuildKit context name if outside the build context, else None
|
||||
"""
|
||||
|
||||
pip_reqs: list[tuple[pathlib.Path, str]]
|
||||
@@ -828,7 +867,9 @@ class LocalDeps(NamedTuple):
|
||||
# if . is in dependencies, use it as working_dir
|
||||
working_dir: Optional[str] = None
|
||||
# if there are local dependencies in parent directories, use additional_contexts
|
||||
additional_contexts: list[pathlib.Path] = None
|
||||
additional_contexts: Optional[list[pathlib.Path]] = None
|
||||
# Packages that need requirements.txt generation
|
||||
pkgs_missing_reqs: Optional[list[ReqGenSpec]] = None
|
||||
|
||||
|
||||
def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps:
|
||||
@@ -864,6 +905,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
faux_pkgs = {}
|
||||
working_dir: Optional[str] = None
|
||||
additional_contexts: list[pathlib.Path] = []
|
||||
pkgs_missing_reqs: list[ReqGenSpec] = []
|
||||
|
||||
for local_dep in config["dependencies"]:
|
||||
if not local_dep.startswith("."):
|
||||
@@ -903,6 +945,35 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
# set working_dir
|
||||
if local_dep == ".":
|
||||
working_dir = f"/deps/{container_name}"
|
||||
requirement_path = f"/deps/{container_name}/requirements.txt"
|
||||
|
||||
# Track packages needing requirements.txt generation (real packages only)
|
||||
if "requirements.txt" not in files:
|
||||
has_pyproject = "pyproject.toml" in files
|
||||
has_uv_lock = "uv.lock" in files
|
||||
|
||||
if has_pyproject:
|
||||
pkg_type = "pyproject"
|
||||
has_lock = has_uv_lock
|
||||
else: # setup.py only
|
||||
pkg_type = "setup"
|
||||
has_lock = False
|
||||
|
||||
container_pkg_path = f"/deps/{container_name}"
|
||||
stage_name = None
|
||||
if config_path.parent not in resolved.parents:
|
||||
stage_name = container_name
|
||||
|
||||
pkgs_missing_reqs.append(
|
||||
ReqGenSpec(
|
||||
host_pkg_path=resolved,
|
||||
container_pkg_path=container_pkg_path,
|
||||
container_req_path=requirement_path,
|
||||
package_type=pkg_type,
|
||||
has_uv_lock=has_lock,
|
||||
stage_name=stage_name,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# We could not find a pyproject.toml or setup.py, so treat as a faux package
|
||||
if any(file == "__init__.py" for file in files):
|
||||
@@ -934,19 +1005,27 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
faux_pkgs[resolved] = (local_dep, container_path)
|
||||
if local_dep == ".":
|
||||
working_dir = container_path
|
||||
requirement_path = f"{container_path}/requirements.txt"
|
||||
|
||||
# If the faux package has a requirements.txt, we'll add
|
||||
# the path to the list of requirements to install.
|
||||
if "requirements.txt" in files:
|
||||
rfile = resolved / "requirements.txt"
|
||||
pip_reqs.append(
|
||||
(
|
||||
rfile,
|
||||
f"{container_path}/requirements.txt",
|
||||
)
|
||||
# If the package has a requirements.txt, we'll add
|
||||
# the path to the list of requirements to install.
|
||||
if "requirements.txt" in files:
|
||||
rfile = resolved / "requirements.txt"
|
||||
pip_reqs.append(
|
||||
(
|
||||
rfile,
|
||||
requirement_path,
|
||||
)
|
||||
)
|
||||
|
||||
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir, additional_contexts)
|
||||
return LocalDeps(
|
||||
pip_reqs,
|
||||
real_pkgs,
|
||||
faux_pkgs,
|
||||
working_dir,
|
||||
additional_contexts,
|
||||
pkgs_missing_reqs,
|
||||
)
|
||||
|
||||
|
||||
def _update_graph_paths(
|
||||
@@ -1241,6 +1320,110 @@ def get_build_tools_to_uninstall(config: Config) -> tuple[str]:
|
||||
)
|
||||
|
||||
|
||||
def _metadata_files(spec: ReqGenSpec) -> list[str]:
|
||||
files = ["pyproject.toml"] if spec.package_type == "pyproject" else ["setup.py"]
|
||||
if spec.package_type == "pyproject" and spec.has_uv_lock:
|
||||
files.append("uv.lock")
|
||||
if spec.package_type == "setup" and (spec.host_pkg_path / "setup.cfg").exists():
|
||||
files.append("setup.cfg")
|
||||
return files
|
||||
|
||||
|
||||
def _get_reqs_gen_cmd(spec: ReqGenSpec) -> str:
|
||||
if spec.package_type == "pyproject" and spec.has_uv_lock:
|
||||
return "uv export --no-hashes --no-dev --no-emit-local -o 'requirements.txt'"
|
||||
if spec.package_type == "pyproject":
|
||||
return f"uv pip compile pyproject.toml -o 'requirements.txt' --constraint {CONSTRAINTS_PATH}"
|
||||
return (
|
||||
f"uv pip compile setup.py -o 'requirements.txt' --constraint {CONSTRAINTS_PATH}"
|
||||
)
|
||||
|
||||
|
||||
def _generate_requirements_from_metadata(
|
||||
config_path: pathlib.Path,
|
||||
local_deps: LocalDeps,
|
||||
pip_installer: Literal["uv", "pip"],
|
||||
) -> str:
|
||||
"""Generate requirements.txt from uv.lock, pyproject.toml, or setup.py.
|
||||
|
||||
This function creates Docker layers that:
|
||||
1. Copy packaging metadata files (pyproject.toml, setup.py, uv.lock, etc.)
|
||||
2. Generate requirements.txt using appropriate uv commands:
|
||||
- `uv export` if uv.lock exists (exact locked versions)
|
||||
- `uv pip compile` for pyproject.toml or setup.py (fresh resolution)
|
||||
|
||||
The generated requirements.txt files are then handled by the existing
|
||||
pip_reqs installation logic, maintaining a single code path for all packages.
|
||||
|
||||
Supports:
|
||||
- pyproject.toml with uv.lock → uv export (preserves lock file versions)
|
||||
- pyproject.toml without uv.lock → uv pip compile (resolve dependencies)
|
||||
- setup.py (with optional setup.cfg) → uv pip compile (resolve dependencies)
|
||||
|
||||
Args:
|
||||
config_path: Path to the langgraph.json config file
|
||||
local_deps: LocalDeps object containing package information
|
||||
pip_installer: Either "uv" or "pip"
|
||||
|
||||
Returns:
|
||||
Docker instruction string for requirements.txt generation,
|
||||
or empty string if not applicable (pip installer or no packages to generate)
|
||||
"""
|
||||
if pip_installer != "uv" or not local_deps.pkgs_missing_reqs:
|
||||
# if installer is pip, we need pip-tools to generate requirements.txt
|
||||
# this doesn't come automatically with pip, so we need to install it
|
||||
# in base images, but ci uses uv, which is where we need the caching
|
||||
# so limit to uv.
|
||||
return ""
|
||||
|
||||
docker_lines = ["# -- Generate requirements.txt for packages without one --"]
|
||||
|
||||
# Layer 1: Copy packaging metadata files needed for requirements generation
|
||||
docker_lines.append("# Copy packaging metadata files")
|
||||
for spec in sorted(
|
||||
local_deps.pkgs_missing_reqs, key=lambda s: s.container_pkg_path
|
||||
):
|
||||
for file_name in _metadata_files(spec):
|
||||
if (
|
||||
local_deps.additional_contexts
|
||||
and spec.host_pkg_path in local_deps.additional_contexts
|
||||
):
|
||||
if not spec.stage_name:
|
||||
raise RuntimeError(
|
||||
f"Package {spec.host_pkg_path} in additional_contexts but has no stage_name"
|
||||
)
|
||||
docker_lines.append(
|
||||
f"COPY --from={spec.stage_name} {file_name} {spec.container_pkg_path}/{file_name}"
|
||||
)
|
||||
else:
|
||||
file_relpath = (spec.host_pkg_path / file_name).relative_to(
|
||||
config_path.parent
|
||||
)
|
||||
docker_lines.append(
|
||||
f"ADD {file_relpath} {spec.container_pkg_path}/{file_name}"
|
||||
)
|
||||
|
||||
# Layer 2: Generate requirements.txt files using appropriate uv commands
|
||||
docker_lines.append("")
|
||||
docker_lines.append("# Generate requirements.txt from packaging metadata")
|
||||
for spec in sorted(
|
||||
local_deps.pkgs_missing_reqs, key=lambda s: s.container_pkg_path
|
||||
):
|
||||
pkg_name = spec.host_pkg_path.name
|
||||
if spec.package_type == "pyproject" and spec.has_uv_lock:
|
||||
docker_lines.append(f"# Generate from uv.lock for {pkg_name}")
|
||||
elif spec.package_type == "pyproject":
|
||||
docker_lines.append(f"# Compile from pyproject.toml for {pkg_name}")
|
||||
else:
|
||||
docker_lines.append(f"# Compile from setup.py for {pkg_name}")
|
||||
docker_lines.append(
|
||||
f"RUN cd '{spec.container_pkg_path}' && {_get_reqs_gen_cmd(spec)}"
|
||||
)
|
||||
|
||||
docker_lines.append("# -- End of requirements.txt generation --")
|
||||
return os.linesep.join(docker_lines)
|
||||
|
||||
|
||||
def python_config_to_docker(
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
@@ -1256,16 +1439,26 @@ def python_config_to_docker(
|
||||
else:
|
||||
pip_installer = "pip"
|
||||
if pip_installer == "uv":
|
||||
install_cmd = "uv pip install --system --prerelease=allow"
|
||||
install_cmd = "uv pip install --system"
|
||||
elif pip_installer == "pip":
|
||||
install_cmd = "pip install"
|
||||
else:
|
||||
raise ValueError(f"Invalid pip_installer: {pip_installer}")
|
||||
|
||||
# configure pip
|
||||
pip_install = f"PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir -c /api/constraints.txt"
|
||||
local_reqs_pip_install = (
|
||||
f"PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir -c {CONSTRAINTS_PATH}"
|
||||
)
|
||||
global_reqs_pip_install = (
|
||||
f"PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir -c {CONSTRAINTS_PATH}"
|
||||
)
|
||||
if config.get("pip_config_file"):
|
||||
pip_install = f"PIP_CONFIG_FILE=/pipconfig.txt {pip_install}"
|
||||
local_reqs_pip_install = (
|
||||
f"PIP_CONFIG_FILE=/pipconfig.txt {local_reqs_pip_install}"
|
||||
)
|
||||
global_reqs_pip_install = (
|
||||
f"PIP_CONFIG_FILE=/pipconfig.txt {global_reqs_pip_install}"
|
||||
)
|
||||
pip_config_file_str = (
|
||||
f"ADD {config['pip_config_file']} /pipconfig.txt"
|
||||
if config.get("pip_config_file")
|
||||
@@ -1282,23 +1475,62 @@ def python_config_to_docker(
|
||||
# Rewrite HTTP app path, so it points to the correct location in the Docker container
|
||||
_update_http_app_path(config_path, config, local_deps)
|
||||
|
||||
pip_pkgs_str = f"RUN {pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
|
||||
if local_deps.pip_reqs:
|
||||
pip_reqs_str = os.linesep.join(
|
||||
(
|
||||
f"COPY --from=outer-{reqpath.name} requirements.txt {destpath}"
|
||||
if reqpath.parent in local_deps.additional_contexts
|
||||
else f"ADD {reqpath.relative_to(config_path.parent)} {destpath}"
|
||||
)
|
||||
for reqpath, destpath in local_deps.pip_reqs
|
||||
)
|
||||
pip_reqs_str += f"{os.linesep}RUN {pip_install} {' '.join('-r ' + r for _, r in local_deps.pip_reqs)}"
|
||||
pip_reqs_str = f"""# -- Installing local requirements --
|
||||
{pip_reqs_str}
|
||||
# -- End of local requirements install --"""
|
||||
pip_pkgs_str = (
|
||||
f"RUN {local_reqs_pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
|
||||
)
|
||||
|
||||
else:
|
||||
pip_reqs_str = ""
|
||||
# Generate requirements.txt layer for packages that need it
|
||||
# This happens BEFORE copying existing requirements.txt files
|
||||
generated_reqs_str = _generate_requirements_from_metadata(
|
||||
config_path, local_deps, pip_installer
|
||||
)
|
||||
# Combine existing requirements.txt with generated ones in a single deterministic layer
|
||||
all_req_paths: list[str] = []
|
||||
copy_lines: list[str] = []
|
||||
|
||||
# Map additional_contexts path -> stage name
|
||||
additional_ctx_stage: dict[pathlib.Path, str] = {}
|
||||
for p in local_deps.additional_contexts or []:
|
||||
if p in local_deps.real_pkgs:
|
||||
additional_ctx_stage[p] = local_deps.real_pkgs[p][1]
|
||||
elif p in local_deps.faux_pkgs:
|
||||
additional_ctx_stage[p] = f"outer-{p.name}"
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Package {p} in additional_contexts but not in real_pkgs or faux_pkgs"
|
||||
)
|
||||
|
||||
# Existing reqs
|
||||
for reqpath, destpath in local_deps.pip_reqs or []:
|
||||
if local_deps.additional_contexts and reqpath.parent in additional_ctx_stage:
|
||||
copy_lines.append(
|
||||
f"COPY --from={additional_ctx_stage[reqpath.parent]} requirements.txt {destpath}"
|
||||
)
|
||||
else:
|
||||
copy_lines.append(
|
||||
f"ADD {reqpath.relative_to(config_path.parent)} {destpath}"
|
||||
)
|
||||
all_req_paths.append(destpath)
|
||||
|
||||
# Generated reqs
|
||||
if (
|
||||
pip_installer == "uv"
|
||||
): # we are only generate a requirements.txt if installer is uv (for now)
|
||||
for spec in local_deps.pkgs_missing_reqs or []:
|
||||
all_req_paths.append(spec.container_req_path)
|
||||
|
||||
pip_reqs_str = ""
|
||||
if all_req_paths:
|
||||
# Stabilize order
|
||||
all_req_paths = sorted(set(all_req_paths))
|
||||
# Install each requirements.txt sequentially. This mimics the previous sequential solver behavior
|
||||
# which allows to adjust/downgrade packages between installs, so no conflict is raised.
|
||||
pip_reqs_str = f"""# -- Installing from requirements.txt files --
|
||||
{os.linesep.join(copy_lines)}
|
||||
{os.linesep.join(f"RUN {local_reqs_pip_install} -r '{p}'" for p in all_req_paths)}
|
||||
# -- End of requirements.txt install --"""
|
||||
|
||||
# generate lock file if real package and lock file missing
|
||||
|
||||
# https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data
|
||||
# https://til.simonwillison.net/python/pyproject
|
||||
@@ -1352,6 +1584,7 @@ ADD {relpath} /deps/{name}
|
||||
install_node_str,
|
||||
pip_config_file_str,
|
||||
pip_pkgs_str,
|
||||
generated_reqs_str,
|
||||
pip_reqs_str,
|
||||
local_pkgs_str,
|
||||
faux_pkgs_str,
|
||||
@@ -1389,7 +1622,7 @@ ADD {relpath} /deps/{name}
|
||||
[
|
||||
"# -- Installing JS dependencies --",
|
||||
f"ENV NODE_VERSION={config.get('node_version') or DEFAULT_NODE_VERSION}",
|
||||
f"RUN cd {local_deps.working_dir} && {_get_node_pm_install_cmd(config_path, config)} && tsx /api/langgraph_api/js/build.mts",
|
||||
f"RUN cd '{local_deps.working_dir}' && {_get_node_pm_install_cmd(config_path, config)} && tsx /api/langgraph_api/js/build.mts",
|
||||
"# -- End of JS dependencies install --",
|
||||
]
|
||||
)
|
||||
@@ -1402,7 +1635,13 @@ ADD {relpath} /deps/{name}
|
||||
installs,
|
||||
"",
|
||||
"# -- Installing all local dependencies --",
|
||||
f"RUN {pip_install} -e /deps/*",
|
||||
f"""RUN for dep in /deps/*; do \
|
||||
echo "Installing $dep"; \
|
||||
if [ -d "$dep" ]; then \
|
||||
echo "Installing $dep"; \
|
||||
(cd "$dep" && {global_reqs_pip_install} .); \
|
||||
fi; \
|
||||
done""",
|
||||
"# -- End of local dependencies install --",
|
||||
os.linesep.join(env_vars),
|
||||
"",
|
||||
|
||||
@@ -40,7 +40,9 @@ def _parse_version(version: str) -> Version:
|
||||
patch = "0"
|
||||
else:
|
||||
major, minor, patch = parts
|
||||
return Version(int(major.lstrip("v")), int(minor), int(patch.split("-")[0]))
|
||||
return Version(
|
||||
int(major.lstrip("v")), int(minor), int(patch.split("-")[0].split("+")[0])
|
||||
)
|
||||
|
||||
|
||||
def check_capabilities(runner) -> DockerCapabilities:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://langgra.ph/schema.json",
|
||||
"dependencies": [".", "../../libs/shared", "../../libs/common"],
|
||||
"graphs": {
|
||||
"agent": "./src/agent/graph.py:graph"
|
||||
|
||||
@@ -577,6 +577,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, /threads routes are removed.\n\nDefault is False.\n"
|
||||
},
|
||||
"enable_custom_route_auth": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, authentication is enabled for custom routes,\nnot just the routes that are protected by default.\n(Routes protected by default include /assistants, /threads, and /runs).\n\nDefault is False. This flag only affects authentication behavior\nif `app` is provided and contains custom routes.\n"
|
||||
},
|
||||
"logging_headers": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -587,6 +591,20 @@
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines which headers are excluded from logging."
|
||||
},
|
||||
"middleware_order": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"auth_first",
|
||||
"middleware_first"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines the order in which to apply server customizations.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
||||
@@ -577,6 +577,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, /threads routes are removed.\n\nDefault is False.\n"
|
||||
},
|
||||
"enable_custom_route_auth": {
|
||||
"type": "boolean",
|
||||
"description": "Optional. If True, authentication is enabled for custom routes,\nnot just the routes that are protected by default.\n(Routes protected by default include /assistants, /threads, and /runs).\n\nDefault is False. This flag only affects authentication behavior\nif `app` is provided and contains custom routes.\n"
|
||||
},
|
||||
"logging_headers": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -587,6 +591,20 @@
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines which headers are excluded from logging."
|
||||
},
|
||||
"middleware_order": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"auth_first",
|
||||
"middleware_first"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Defines the order in which to apply server customizations.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
||||
@@ -15,7 +15,7 @@ from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Versi
|
||||
from langgraph_cli.util import clean_empty_lines
|
||||
|
||||
FORMATTED_CLEANUP_LINES = _get_pip_cleanup_lines(
|
||||
install_cmd="uv pip install --system --prerelease=allow",
|
||||
install_cmd="uv pip install --system",
|
||||
to_uninstall=("pip", "setuptools", "wheel"),
|
||||
pip_installer="uv",
|
||||
)
|
||||
@@ -142,6 +142,21 @@ services:
|
||||
- cli_1: {str(pathlib.Path(__file__).parent.parent.parent.parent.absolute())}
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Generate requirements.txt for packages without one --
|
||||
# Copy packaging metadata files
|
||||
ADD pyproject.toml /deps/cli/pyproject.toml
|
||||
COPY --from=cli_1 pyproject.toml /deps/cli_1/pyproject.toml
|
||||
COPY --from=cli_1 uv.lock /deps/cli_1/uv.lock
|
||||
# Generate requirements.txt from packaging metadata
|
||||
# Compile from pyproject.toml for cli
|
||||
RUN cd '/deps/cli' && uv pip compile pyproject.toml -o 'requirements.txt' --constraint /api/constraints.txt
|
||||
# Generate from uv.lock for cli
|
||||
RUN cd '/deps/cli_1' && uv export --no-hashes --no-dev -o 'requirements.txt'
|
||||
# -- End of requirements.txt generation --
|
||||
# -- Installing from requirements.txt files --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r '/deps/cli/requirements.txt'
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r '/deps/cli_1/requirements.txt'
|
||||
# -- End of requirements.txt install --
|
||||
# -- Adding local package . --
|
||||
ADD . /deps/cli
|
||||
# -- End of local package . --
|
||||
@@ -149,7 +164,7 @@ services:
|
||||
COPY --from=cli_1 . /deps/cli_1
|
||||
# -- End of local package ../../.. --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
|
||||
@@ -20,7 +20,7 @@ from langgraph_cli.config import (
|
||||
from langgraph_cli.util import clean_empty_lines
|
||||
|
||||
FORMATTED_CLEANUP_LINES = _get_pip_cleanup_lines(
|
||||
install_cmd="uv pip install --system --prerelease=allow",
|
||||
install_cmd="uv pip install --system",
|
||||
to_uninstall=("pip", "setuptools", "wheel"),
|
||||
pip_installer="uv",
|
||||
)
|
||||
@@ -420,10 +420,18 @@ def test_config_to_docker_simple():
|
||||
)
|
||||
expected_docker_stdin = f"""\
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Installing local requirements --
|
||||
COPY --from=outer-requirements.txt requirements.txt /deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -r /deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||
# -- End of local requirements install --
|
||||
# -- Generate requirements.txt for packages without one --
|
||||
# Copy packaging metadata files
|
||||
COPY --from=examples pyproject.toml /deps/examples/pyproject.toml
|
||||
# Generate requirements.txt from packaging metadata
|
||||
# Compile from pyproject.toml for examples
|
||||
RUN cd '/deps/examples' && uv pip compile pyproject.toml -o 'requirements.txt' --constraint /api/constraints.txt
|
||||
# -- End of requirements.txt generation --
|
||||
# -- Installing from requirements.txt files --
|
||||
COPY --from=outer-graphs_reqs_a requirements.txt /deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r '/deps/examples/requirements.txt'
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r '/deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt'
|
||||
# -- End of requirements.txt install --
|
||||
# -- Adding local package ../../examples --
|
||||
COPY --from=examples . /deps/examples
|
||||
# -- End of local package ../../examples --
|
||||
@@ -456,7 +464,7 @@ RUN set -ex && \\
|
||||
done
|
||||
# -- End of non-package dependency graphs_reqs_a --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGGRAPH_HTTP='{{"app": "/deps/examples/my_app.py:app"}}'
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
@@ -512,7 +520,7 @@ RUN set -ex && \\
|
||||
done
|
||||
# -- End of non-package dependency tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}'
|
||||
"""
|
||||
@@ -559,7 +567,7 @@ RUN set -ex && \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}'
|
||||
"""
|
||||
@@ -621,7 +629,7 @@ RUN set -ex && \\
|
||||
done
|
||||
# -- End of non-package dependency graphs --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-graphs/src/agent.py:graph"}}'
|
||||
{FORMATTED_CLEANUP_LINES}\
|
||||
@@ -653,11 +661,21 @@ dependencies = ["langchain"]"""
|
||||
os.remove(pyproject_path)
|
||||
expected_docker_stdin = (
|
||||
"""FROM langchain/langgraph-api:3.11
|
||||
# -- Generate requirements.txt for packages without one --
|
||||
# Copy packaging metadata files
|
||||
ADD pyproject.toml /deps/unit_tests/pyproject.toml
|
||||
# Generate requirements.txt from packaging metadata
|
||||
# Compile from pyproject.toml for unit_tests
|
||||
RUN cd '/deps/unit_tests' && uv pip compile pyproject.toml -o 'requirements.txt' --constraint /api/constraints.txt
|
||||
# -- End of requirements.txt generation --
|
||||
# -- Installing from requirements.txt files --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r '/deps/unit_tests/requirements.txt'
|
||||
# -- End of requirements.txt install --
|
||||
# -- Adding local package . --
|
||||
ADD . /deps/unit_tests
|
||||
# -- End of local package . --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}'
|
||||
"""
|
||||
@@ -689,7 +707,7 @@ def test_config_to_docker_end_to_end():
|
||||
ARG meow
|
||||
ARG foo
|
||||
ADD pipconfig.txt /pipconfig.txt
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt langchain langchain_openai
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt langchain langchain_openai
|
||||
# -- Adding non-package dependency graphs --
|
||||
ADD ./graphs/ /deps/outer-graphs/src
|
||||
RUN set -ex && \\
|
||||
@@ -705,7 +723,7 @@ RUN set -ex && \\
|
||||
done
|
||||
# -- End of non-package dependency graphs --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-graphs/src/agent.py:graph"}}'
|
||||
{FORMATTED_CLEANUP_LINES}"""
|
||||
@@ -811,14 +829,14 @@ RUN set -ex && \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGGRAPH_UI='{{"agent": "./graphs/agent.ui.jsx"}}'
|
||||
ENV LANGGRAPH_UI_CONFIG='{{"shared": ["nuqs"]}}'
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
# -- Installing JS dependencies --
|
||||
ENV NODE_VERSION=20
|
||||
RUN cd /deps/outer-unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
|
||||
RUN cd '/deps/outer-unit_tests/unit_tests' && npm i && tsx /api/langgraph_api/js/build.mts
|
||||
# -- End of JS dependencies install --
|
||||
{FORMATTED_CLEANUP_LINES}
|
||||
WORKDIR /deps/outer-unit_tests/unit_tests"""
|
||||
@@ -857,12 +875,12 @@ RUN set -ex && \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"python": "/deps/outer-unit_tests/unit_tests/multiplatform/python.py:graph", "js": "/deps/outer-unit_tests/unit_tests/multiplatform/js.mts:graph"}}'
|
||||
# -- Installing JS dependencies --
|
||||
ENV NODE_VERSION=22
|
||||
RUN cd /deps/outer-unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
|
||||
RUN cd '/deps/outer-unit_tests/unit_tests' && npm i && tsx /api/langgraph_api/js/build.mts
|
||||
# -- End of JS dependencies install --
|
||||
{FORMATTED_CLEANUP_LINES}
|
||||
WORKDIR /deps/outer-unit_tests/unit_tests"""
|
||||
@@ -887,7 +905,7 @@ def test_config_to_docker_pip_installer():
|
||||
docker_auto, _ = config_to_docker(
|
||||
PATH_TO_CONFIG, config_auto, "langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system --prerelease=allow" in docker_auto
|
||||
assert "uv pip install --system " in docker_auto
|
||||
assert "rm /usr/bin/uv /usr/bin/uvx" in docker_auto
|
||||
|
||||
# Test explicit pip setting
|
||||
@@ -895,7 +913,7 @@ def test_config_to_docker_pip_installer():
|
||||
docker_pip, _ = config_to_docker(
|
||||
PATH_TO_CONFIG, config_pip, "langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system --prerelease=allow" not in docker_pip
|
||||
assert "uv pip install --system " not in docker_pip
|
||||
assert "pip install" in docker_pip
|
||||
assert "rm /usr/bin/uv" not in docker_pip
|
||||
|
||||
@@ -904,7 +922,7 @@ def test_config_to_docker_pip_installer():
|
||||
docker_uv, _ = config_to_docker(
|
||||
PATH_TO_CONFIG, config_uv, "langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system --prerelease=allow" in docker_uv
|
||||
assert "uv pip install --system " in docker_uv
|
||||
assert "rm /usr/bin/uv /usr/bin/uvx" in docker_uv
|
||||
|
||||
# Test auto behavior with older image (should use pip)
|
||||
@@ -914,7 +932,7 @@ def test_config_to_docker_pip_installer():
|
||||
docker_auto_old, _ = config_to_docker(
|
||||
PATH_TO_CONFIG, config_auto_old, "langchain/langgraph-api:0.2.46"
|
||||
)
|
||||
assert "uv pip install --system --prerelease=allow" not in docker_auto_old
|
||||
assert "uv pip install --system " not in docker_auto_old
|
||||
assert "pip install" in docker_auto_old
|
||||
assert "rm /usr/bin/uv" not in docker_auto_old
|
||||
|
||||
@@ -923,7 +941,7 @@ def test_config_to_docker_pip_installer():
|
||||
docker_default, _ = config_to_docker(
|
||||
PATH_TO_CONFIG, config_default, "langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system --prerelease=allow" in docker_default
|
||||
assert "uv pip install --system " in docker_default
|
||||
|
||||
|
||||
def test_config_retain_build_tools():
|
||||
@@ -998,7 +1016,7 @@ def test_config_to_compose_simple_config():
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
@@ -1039,7 +1057,7 @@ def test_config_to_compose_env_vars():
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
@@ -1084,7 +1102,7 @@ def test_config_to_compose_env_file():
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
@@ -1122,7 +1140,7 @@ def test_config_to_compose_watch():
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
@@ -1169,7 +1187,7 @@ def test_config_to_compose_end_to_end():
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --prerelease=allow --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import pytest
|
||||
|
||||
from langgraph_cli.docker import (
|
||||
DEFAULT_POSTGRES_URI,
|
||||
DockerCapabilities,
|
||||
Version,
|
||||
_parse_version,
|
||||
compose,
|
||||
)
|
||||
from langgraph_cli.util import clean_empty_lines
|
||||
@@ -363,3 +366,22 @@ services:
|
||||
REDIS_URI: redis://langgraph-redis:6379
|
||||
POSTGRES_URI: {DEFAULT_POSTGRES_URI}"""
|
||||
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_str,expected",
|
||||
[
|
||||
("1.2.3", Version(1, 2, 3)),
|
||||
("v1.2.3", Version(1, 2, 3)),
|
||||
("1.2.3-alpha", Version(1, 2, 3)),
|
||||
("1.2.3+1", Version(1, 2, 3)),
|
||||
("1.2.3-alpha+build", Version(1, 2, 3)),
|
||||
("1.2", Version(1, 2, 0)),
|
||||
("1", Version(1, 0, 0)),
|
||||
("v28.1.1+1", Version(28, 1, 1)),
|
||||
("2.0.0-beta.1+exp.sha.5114f85", Version(2, 0, 0)),
|
||||
("v3.4.5-rc1+build.123", Version(3, 4, 5)),
|
||||
],
|
||||
)
|
||||
def test_parse_version_w_edge_cases(input_str, expected):
|
||||
assert _parse_version(input_str) == expected
|
||||
@@ -0,0 +1,188 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from langgraph_cli.util import clean_empty_lines, warn_non_wolfi_distro
|
||||
|
||||
|
||||
def test_clean_empty_lines():
|
||||
"""Test clean_empty_lines function."""
|
||||
# Test with empty lines
|
||||
input_str = "line1\n\nline2\n\nline3"
|
||||
result = clean_empty_lines(input_str)
|
||||
assert result == "line1\nline2\nline3"
|
||||
|
||||
# Test with no empty lines
|
||||
input_str = "line1\nline2\nline3"
|
||||
result = clean_empty_lines(input_str)
|
||||
assert result == "line1\nline2\nline3"
|
||||
|
||||
# Test with only empty lines
|
||||
input_str = "\n\n\n"
|
||||
result = clean_empty_lines(input_str)
|
||||
assert result == ""
|
||||
|
||||
# Test empty string
|
||||
input_str = ""
|
||||
result = clean_empty_lines(input_str)
|
||||
assert result == ""
|
||||
|
||||
|
||||
def test_warn_non_wolfi_distro_with_debian(capsys):
|
||||
"""Test that warning is shown when image_distro is 'debian'."""
|
||||
config = {"image_distro": "debian"}
|
||||
|
||||
warn_non_wolfi_distro(config)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert (
|
||||
"⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security."
|
||||
in captured.out
|
||||
)
|
||||
assert (
|
||||
"Wolfi is a security-oriented, minimal Linux distribution designed for containers."
|
||||
in captured.out
|
||||
)
|
||||
assert (
|
||||
'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.'
|
||||
in captured.out
|
||||
)
|
||||
|
||||
|
||||
def test_warn_non_wolfi_distro_with_default_debian(capsys):
|
||||
"""Test that warning is shown when image_distro is missing (defaults to debian)."""
|
||||
config = {} # No image_distro key, should default to debian
|
||||
|
||||
warn_non_wolfi_distro(config)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert (
|
||||
"⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security."
|
||||
in captured.out
|
||||
)
|
||||
assert (
|
||||
"Wolfi is a security-oriented, minimal Linux distribution designed for containers."
|
||||
in captured.out
|
||||
)
|
||||
assert (
|
||||
'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.'
|
||||
in captured.out
|
||||
)
|
||||
|
||||
|
||||
def test_warn_non_wolfi_distro_with_wolfi(capsys):
|
||||
"""Test that no warning is shown when image_distro is 'wolfi'."""
|
||||
config = {"image_distro": "wolfi"}
|
||||
|
||||
warn_non_wolfi_distro(config)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "" # No output should be generated
|
||||
|
||||
|
||||
def test_warn_non_wolfi_distro_with_other_distro(capsys):
|
||||
"""Test that warning is shown when image_distro is something other than 'wolfi'."""
|
||||
config = {"image_distro": "ubuntu"}
|
||||
|
||||
warn_non_wolfi_distro(config)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert (
|
||||
"⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security."
|
||||
in captured.out
|
||||
)
|
||||
assert (
|
||||
"Wolfi is a security-oriented, minimal Linux distribution designed for containers."
|
||||
in captured.out
|
||||
)
|
||||
assert (
|
||||
'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.'
|
||||
in captured.out
|
||||
)
|
||||
|
||||
|
||||
def test_warn_non_wolfi_distro_output_formatting():
|
||||
"""Test that the warning output is properly formatted with colors and empty line."""
|
||||
config = {"image_distro": "debian"}
|
||||
|
||||
with patch("click.secho") as mock_secho:
|
||||
warn_non_wolfi_distro(config)
|
||||
|
||||
# Verify click.secho was called with the correct parameters
|
||||
expected_calls = [
|
||||
(
|
||||
(
|
||||
"⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security.",
|
||||
),
|
||||
{"fg": "yellow", "bold": True},
|
||||
),
|
||||
(
|
||||
(
|
||||
" Wolfi is a security-oriented, minimal Linux distribution designed for containers.",
|
||||
),
|
||||
{"fg": "yellow"},
|
||||
),
|
||||
(
|
||||
(
|
||||
' To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.',
|
||||
),
|
||||
{"fg": "yellow"},
|
||||
),
|
||||
(
|
||||
("",), # Empty line
|
||||
{},
|
||||
),
|
||||
]
|
||||
|
||||
assert mock_secho.call_count == 4
|
||||
for i, (expected_args, expected_kwargs) in enumerate(expected_calls):
|
||||
actual_call = mock_secho.call_args_list[i]
|
||||
assert actual_call.args == expected_args
|
||||
assert actual_call.kwargs == expected_kwargs
|
||||
|
||||
|
||||
def test_warn_non_wolfi_distro_various_configs(capsys):
|
||||
"""Test warn_non_wolfi_distro with various config scenarios."""
|
||||
test_cases = [
|
||||
# (config, should_warn, description)
|
||||
({"image_distro": "debian"}, True, "explicit debian"),
|
||||
({"image_distro": "wolfi"}, False, "explicit wolfi"),
|
||||
({}, True, "missing image_distro (defaults to debian)"),
|
||||
({"image_distro": "alpine"}, True, "other distro"),
|
||||
({"image_distro": "ubuntu"}, True, "ubuntu distro"),
|
||||
({"other_config": "value"}, True, "unrelated config keys"),
|
||||
]
|
||||
|
||||
for config, should_warn, description in test_cases:
|
||||
# Clear any previous output
|
||||
capsys.readouterr()
|
||||
|
||||
warn_non_wolfi_distro(config)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
if should_warn:
|
||||
assert "⚠️ Security Recommendation" in captured.out, (
|
||||
f"Should warn for {description}"
|
||||
)
|
||||
assert "Wolfi" in captured.out, f"Should mention Wolfi for {description}"
|
||||
else:
|
||||
assert captured.out == "", f"Should not warn for {description}"
|
||||
|
||||
|
||||
def test_warn_non_wolfi_distro_return_value():
|
||||
"""Test that warn_non_wolfi_distro returns None."""
|
||||
config = {"image_distro": "debian"}
|
||||
result = warn_non_wolfi_distro(config)
|
||||
assert result is None
|
||||
|
||||
config = {"image_distro": "wolfi"}
|
||||
result = warn_non_wolfi_distro(config)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_warn_non_wolfi_distro_does_not_modify_config():
|
||||
"""Test that warn_non_wolfi_distro does not modify the input config."""
|
||||
original_config = {"image_distro": "debian", "other_key": "value"}
|
||||
config_copy = original_config.copy()
|
||||
|
||||
warn_non_wolfi_distro(config_copy)
|
||||
|
||||
assert config_copy == original_config # Config should remain unchanged
|
||||
Generated
+618
-401
File diff suppressed because it is too large.
Load diff
@@ -76,7 +76,7 @@ test:
|
||||
test_parallel:
|
||||
make start-services &&\
|
||||
make start-dev-server &&\
|
||||
uv run pytest -n auto --dist worksteal $(TEST); \
|
||||
uv run pytest -n auto --dist worksteal $(TEST) --lf --snapshot-update; \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-services; \
|
||||
make stop-dev-server; \
|
||||
|
||||
@@ -2,6 +2,7 @@ import random
|
||||
from uuid import uuid4
|
||||
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from pyperf._runner import Runner
|
||||
from uvloop import new_event_loop
|
||||
|
||||
@@ -11,7 +12,6 @@ from bench.react_agent import react_agent
|
||||
from bench.sequential import create_sequential
|
||||
from bench.wide_dict import wide_dict
|
||||
from bench.wide_state import wide_state
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.pregel import Pregel
|
||||
|
||||
|
||||
@@ -114,7 +114,6 @@ if __name__ == "__main__":
|
||||
import time
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
graph = fanout_to_subgraph().compile(checkpointer=InMemorySaver())
|
||||
|
||||
@@ -303,7 +303,6 @@ if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
graph = pydantic_state(1000).compile(checkpointer=InMemorySaver())
|
||||
|
||||
@@ -8,9 +8,9 @@ from langchain_core.language_models.fake_chat_models import (
|
||||
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
|
||||
from langchain_core.outputs import ChatGeneration, ChatResult
|
||||
from langchain_core.tools import StructuredTool
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.prebuilt.chat_agent_executor import create_react_agent
|
||||
|
||||
from langgraph.pregel import Pregel
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
graph = react_agent(100, checkpointer=InMemorySaver())
|
||||
|
||||
@@ -129,7 +129,6 @@ if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
graph = wide_dict(1000).compile(checkpointer=InMemorySaver())
|
||||
|
||||
@@ -139,7 +139,6 @@ if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
graph = wide_state(1000).compile(checkpointer=InMemorySaver())
|
||||
|
||||
@@ -17,6 +17,7 @@ from langchain_core.runnables.config import (
|
||||
COPIABLE_KEYS,
|
||||
var_child_runnable_config,
|
||||
)
|
||||
from langgraph.checkpoint.base import CheckpointMetadata
|
||||
|
||||
from langgraph._internal._constants import (
|
||||
CONF,
|
||||
@@ -26,7 +27,6 @@ from langgraph._internal._constants import (
|
||||
NS_END,
|
||||
NS_SEP,
|
||||
)
|
||||
from langgraph.checkpoint.base import CheckpointMetadata
|
||||
|
||||
DEFAULT_RECURSION_LIMIT = int(getenv("LANGGRAPH_DEFAULT_RECURSION_LIMIT", "25"))
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ from langchain_core.runnables.config import (
|
||||
)
|
||||
from langchain_core.runnables.utils import Input, Output
|
||||
from langchain_core.tracers.langchain import LangChainTracer
|
||||
from langgraph.store.base import BaseStore
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from langgraph._internal._config import (
|
||||
@@ -54,7 +55,6 @@ from langgraph._internal._constants import (
|
||||
CONFIG_KEY_RUNTIME,
|
||||
)
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
try:
|
||||
@@ -345,7 +345,7 @@ class RunnableCallable(Runnable):
|
||||
args = (input,)
|
||||
kwargs = {**self.kwargs, **kwargs}
|
||||
|
||||
runtime = config[CONF].get(CONFIG_KEY_RUNTIME)
|
||||
runtime = config.get(CONF, {}).get(CONFIG_KEY_RUNTIME)
|
||||
|
||||
for kw, (runtime_key, default) in self.func_accepts.items():
|
||||
# If the kwarg is already set, use the set value
|
||||
@@ -417,7 +417,7 @@ class RunnableCallable(Runnable):
|
||||
args = (input,)
|
||||
kwargs = {**self.kwargs, **kwargs}
|
||||
|
||||
runtime = config[CONF].get(CONFIG_KEY_RUNTIME)
|
||||
runtime = config.get(CONF, {}).get(CONFIG_KEY_RUNTIME)
|
||||
|
||||
for kw, (runtime_key, default) in self.func_accepts.items():
|
||||
# If the kwarg has already been set, use the set value
|
||||
|
||||
@@ -4,9 +4,9 @@ from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.runnables.config import var_child_runnable_config
|
||||
from langgraph.store.base import BaseStore
|
||||
|
||||
from langgraph._internal._constants import CONF, CONFIG_KEY_RUNTIME
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
|
||||
@@ -66,14 +66,17 @@ def get_store() -> BaseStore:
|
||||
store = InMemoryStore()
|
||||
store.put(("values",), "foo", {"bar": 2})
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
foo: int
|
||||
|
||||
|
||||
def my_node(state: State):
|
||||
my_store = get_store()
|
||||
stored_value = my_store.get(("values",), "foo").value["bar"]
|
||||
return {"foo": stored_value + 1}
|
||||
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_node(my_node)
|
||||
@@ -85,7 +88,7 @@ def get_store() -> BaseStore:
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'foo': 3}
|
||||
{"foo": 3}
|
||||
```
|
||||
|
||||
Example: Using with functional API
|
||||
@@ -97,16 +100,19 @@ def get_store() -> BaseStore:
|
||||
store = InMemoryStore()
|
||||
store.put(("values",), "foo", {"bar": 2})
|
||||
|
||||
|
||||
@task
|
||||
def my_task(value: int):
|
||||
my_store = get_store()
|
||||
stored_value = my_store.get(("values",), "foo").value["bar"]
|
||||
return stored_value + 1
|
||||
|
||||
|
||||
@entrypoint(store=store)
|
||||
def workflow(value: int):
|
||||
return my_task(value).result()
|
||||
|
||||
|
||||
workflow.invoke(1)
|
||||
```
|
||||
|
||||
@@ -134,14 +140,17 @@ def get_stream_writer() -> StreamWriter:
|
||||
from langgraph.graph import StateGraph, START
|
||||
from langgraph.config import get_stream_writer
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
foo: int
|
||||
|
||||
|
||||
def my_node(state: State):
|
||||
my_stream_writer = get_stream_writer()
|
||||
my_stream_writer({"custom_data": "Hello!"})
|
||||
return {"foo": state["foo"] + 1}
|
||||
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_node(my_node)
|
||||
@@ -154,7 +163,7 @@ def get_stream_writer() -> StreamWriter:
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'custom_data': 'Hello!'}
|
||||
{"custom_data": "Hello!"}
|
||||
```
|
||||
|
||||
Example: Using with functional API
|
||||
@@ -162,22 +171,25 @@ def get_stream_writer() -> StreamWriter:
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.config import get_stream_writer
|
||||
|
||||
|
||||
@task
|
||||
def my_task(value: int):
|
||||
my_stream_writer = get_stream_writer()
|
||||
my_stream_writer({"custom_data": "Hello!"})
|
||||
return value + 1
|
||||
|
||||
|
||||
@entrypoint(store=store)
|
||||
def workflow(value: int):
|
||||
return my_task(value).result()
|
||||
|
||||
|
||||
for chunk in workflow.stream(1, stream_mode="custom"):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'custom_data': 'Hello!'}
|
||||
{"custom_data": "Hello!"}
|
||||
```
|
||||
"""
|
||||
runtime = get_config()[CONF][CONFIG_KEY_RUNTIME]
|
||||
|
||||
@@ -5,10 +5,10 @@ from enum import Enum
|
||||
from typing import Any
|
||||
from warnings import warn
|
||||
|
||||
from typing_extensions import deprecated
|
||||
|
||||
# EmptyChannelError is re-exported from langgraph.channels.base
|
||||
from langgraph.checkpoint.base import EmptyChannelError # noqa: F401
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from langgraph.types import Command, Interrupt
|
||||
from langgraph.warnings import LangGraphDeprecatedSinceV10
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import functools
|
||||
import inspect
|
||||
import warnings
|
||||
@@ -18,14 +16,15 @@ from typing import (
|
||||
overload,
|
||||
)
|
||||
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.store.base import BaseStore
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from langgraph._internal._constants import CACHE_NS_WRITES, PREVIOUS
|
||||
from langgraph._internal._typing import MISSING, DeprecatedKwargs
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.pregel import Pregel
|
||||
from langgraph.pregel._call import (
|
||||
@@ -38,7 +37,6 @@ from langgraph.pregel._call import (
|
||||
)
|
||||
from langgraph.pregel._read import PregelNode
|
||||
from langgraph.pregel._write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import _DC_KWARGS, CachePolicy, RetryPolicy, StreamMode
|
||||
from langgraph.typing import ContextT
|
||||
from langgraph.warnings import LangGraphDeprecatedSinceV05, LangGraphDeprecatedSinceV10
|
||||
@@ -49,7 +47,7 @@ __all__ = ("task", "entrypoint")
|
||||
class _TaskFunction(Generic[P, T]):
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[P, T],
|
||||
func: Callable[P, Awaitable[T]] | Callable[P, T],
|
||||
*,
|
||||
retry_policy: Sequence[RetryPolicy],
|
||||
cache_policy: CachePolicy[Callable[P, str | bytes]] | None = None,
|
||||
@@ -60,7 +58,7 @@ class _TaskFunction(Generic[P, T]):
|
||||
# handle class methods
|
||||
# NOTE: we're modifying the instance method to avoid modifying
|
||||
# the original class method in case it's shared across multiple tasks
|
||||
instance_method = functools.partial(func.__func__, func.__self__) # type: ignore [attr-defined]
|
||||
instance_method = functools.partial(func.__func__, func.__self__) # type: ignore [union-attr]
|
||||
instance_method.__name__ = name # type: ignore [attr-defined]
|
||||
func = instance_method
|
||||
else:
|
||||
@@ -95,6 +93,7 @@ class _TaskFunction(Generic[P, T]):
|
||||
|
||||
@overload
|
||||
def task(
|
||||
__func_or_none__: None = None,
|
||||
*,
|
||||
name: str | None = None,
|
||||
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
||||
@@ -107,9 +106,11 @@ def task(
|
||||
|
||||
|
||||
@overload
|
||||
def task(
|
||||
__func_or_none__: Callable[P, Awaitable[T]] | Callable[P, T],
|
||||
) -> _TaskFunction[P, T]: ...
|
||||
def task(__func_or_none__: Callable[P, Awaitable[T]]) -> _TaskFunction[P, T]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def task(__func_or_none__: Callable[P, T]) -> _TaskFunction[P, T]: ...
|
||||
|
||||
|
||||
def task(
|
||||
@@ -149,16 +150,19 @@ def task(
|
||||
```python
|
||||
from langgraph.func import entrypoint, task
|
||||
|
||||
|
||||
@task
|
||||
def add_one(a: int) -> int:
|
||||
return a + 1
|
||||
|
||||
|
||||
@entrypoint()
|
||||
def add_one(numbers: list[int]) -> list[int]:
|
||||
futures = [add_one(n) for n in numbers]
|
||||
results = [f.result() for f in futures]
|
||||
return results
|
||||
|
||||
|
||||
# Call the entrypoint
|
||||
add_one.invoke([1, 2, 3]) # Returns [2, 3, 4]
|
||||
```
|
||||
@@ -168,15 +172,18 @@ def task(
|
||||
import asyncio
|
||||
from langgraph.func import entrypoint, task
|
||||
|
||||
|
||||
@task
|
||||
async def add_one(a: int) -> int:
|
||||
return a + 1
|
||||
|
||||
|
||||
@entrypoint()
|
||||
async def add_one(numbers: list[int]) -> list[int]:
|
||||
futures = [add_one(n) for n in numbers]
|
||||
return asyncio.gather(*futures)
|
||||
|
||||
|
||||
# Call the entrypoint
|
||||
await add_one.ainvoke([1, 2, 3]) # Returns [2, 3, 4]
|
||||
```
|
||||
@@ -200,7 +207,7 @@ def task(
|
||||
|
||||
def decorator(
|
||||
func: Callable[P, Awaitable[T]] | Callable[P, T],
|
||||
) -> Callable[P, concurrent.futures.Future[T]] | Callable[P, asyncio.Future[T]]:
|
||||
) -> Callable[P, SyncAsyncFuture[T]]:
|
||||
return _TaskFunction(
|
||||
func, retry_policy=retry_policies, cache_policy=cache_policy, name=name
|
||||
)
|
||||
@@ -341,15 +348,13 @@ class entrypoint(Generic[ContextT]):
|
||||
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
|
||||
@entrypoint(checkpointer=InMemorySaver())
|
||||
def my_workflow(input_data: str, previous: Optional[str] = None) -> str:
|
||||
return "world"
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread"
|
||||
}
|
||||
}
|
||||
|
||||
config = {"configurable": {"thread_id": "some_thread"}}
|
||||
my_workflow.invoke("hello", config)
|
||||
```
|
||||
|
||||
@@ -366,19 +371,21 @@ class entrypoint(Generic[ContextT]):
|
||||
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
|
||||
@entrypoint(checkpointer=InMemorySaver())
|
||||
def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]:
|
||||
def my_workflow(
|
||||
number: int,
|
||||
*,
|
||||
previous: Any = None,
|
||||
) -> entrypoint.final[int, int]:
|
||||
previous = previous or 0
|
||||
# This will return the previous value to the caller, saving
|
||||
# 2 * number to the checkpoint, which will be used in the next invocation
|
||||
# for the `previous` parameter.
|
||||
return entrypoint.final(value=previous, save=2 * number)
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread"
|
||||
}
|
||||
}
|
||||
|
||||
config = {"configurable": {"thread_id": "some_thread"}}
|
||||
|
||||
my_workflow.invoke(3, config) # 0 (previous was None)
|
||||
my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation)
|
||||
@@ -433,19 +440,21 @@ class entrypoint(Generic[ContextT]):
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
|
||||
@entrypoint(checkpointer=InMemorySaver())
|
||||
def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]:
|
||||
def my_workflow(
|
||||
number: int,
|
||||
*,
|
||||
previous: Any = None,
|
||||
) -> entrypoint.final[int, int]:
|
||||
previous = previous or 0
|
||||
# This will return the previous value to the caller, saving
|
||||
# 2 * number to the checkpoint, which will be used in the next invocation
|
||||
# for the `previous` parameter.
|
||||
return entrypoint.final(value=previous, save=2 * number)
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
my_workflow.invoke(3, config) # 0 (previous was None)
|
||||
my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation)
|
||||
|
||||
@@ -6,11 +6,11 @@ from dataclasses import dataclass
|
||||
from typing import Any, Generic, Protocol, Union
|
||||
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langgraph.store.base import BaseStore
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from langgraph._internal._typing import EMPTY_SEQ
|
||||
from langgraph.runtime import Runtime
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import CachePolicy, RetryPolicy, StreamWriter
|
||||
from langgraph.typing import ContextT, NodeInputT, NodeInputT_contra
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ def add_messages(
|
||||
Example:
|
||||
```python title="Basic usage"
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
|
||||
msgs1 = [HumanMessage(content="Hello", id="1")]
|
||||
msgs2 = [AIMessage(content="Hi there!", id="2")]
|
||||
add_messages(msgs1, msgs2)
|
||||
@@ -110,9 +111,11 @@ def add_messages(
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph import StateGraph
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list, add_messages]
|
||||
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("chatbot", lambda state: {"messages": [("assistant", "Hello")]})
|
||||
builder.set_entry_point("chatbot")
|
||||
@@ -127,30 +130,35 @@ def add_messages(
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph import StateGraph, add_messages
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list, add_messages(format='langchain-openai')]
|
||||
messages: Annotated[list, add_messages(format="langchain-openai")]
|
||||
|
||||
|
||||
def chatbot_node(state: State) -> list:
|
||||
return {"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Here's an image:",
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/jpeg",
|
||||
"data": "1234",
|
||||
return {
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Here's an image:",
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
]}
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/jpeg",
|
||||
"data": "1234",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("chatbot", chatbot_node)
|
||||
|
||||
@@ -24,6 +24,9 @@ from typing import (
|
||||
)
|
||||
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
from langgraph.store.base import BaseStore
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
from typing_extensions import NotRequired, Required, Self, Unpack, is_typeddict
|
||||
|
||||
@@ -41,7 +44,6 @@ from langgraph._internal._fields import (
|
||||
from langgraph._internal._pydantic import create_model
|
||||
from langgraph._internal._runnable import coerce_to_runnable
|
||||
from langgraph._internal._typing import EMPTY_SEQ, MISSING, DeprecatedKwargs
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.channels.binop import BinaryOperatorAggregate
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
@@ -50,7 +52,6 @@ from langgraph.channels.named_barrier_value import (
|
||||
NamedBarrierValue,
|
||||
NamedBarrierValueAfterFinish,
|
||||
)
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
from langgraph.constants import END, START, TAG_HIDDEN
|
||||
from langgraph.errors import (
|
||||
ErrorCode,
|
||||
@@ -71,7 +72,6 @@ from langgraph.pregel._write import (
|
||||
ChannelWriteEntry,
|
||||
ChannelWriteTupleEntry,
|
||||
)
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import (
|
||||
All,
|
||||
CachePolicy,
|
||||
@@ -141,25 +141,31 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
|
||||
def reducer(a: list, b: int | None) -> list:
|
||||
if b is not None:
|
||||
return a + [b]
|
||||
return a
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
x: Annotated[list, reducer]
|
||||
|
||||
|
||||
class Context(TypedDict):
|
||||
r: float
|
||||
|
||||
|
||||
graph = StateGraph(state_schema=State, context_schema=Context)
|
||||
|
||||
|
||||
def node(state: State, runtime: Runtime[Context]) -> dict:
|
||||
r = runtime.context.get("r", 1.0)
|
||||
x = state["x"][-1]
|
||||
next_value = x * r * (1 - x)
|
||||
return {"x": next_value}
|
||||
|
||||
|
||||
graph.add_node("A", node)
|
||||
graph.set_entry_point("A")
|
||||
graph.set_finish_point("A")
|
||||
@@ -385,12 +391,15 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.graph import START, StateGraph
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
x: int
|
||||
|
||||
|
||||
def my_node(state: State, config: RunnableConfig) -> State:
|
||||
return {"x": state["x"] + 1}
|
||||
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node(my_node) # node name will be 'my_node'
|
||||
builder.add_edge(START, "my_node")
|
||||
@@ -1360,10 +1369,14 @@ def _get_channel(
|
||||
def _is_field_channel(typ: type[Any]) -> BaseChannel | None:
|
||||
if hasattr(typ, "__metadata__"):
|
||||
meta = typ.__metadata__
|
||||
if len(meta) >= 1 and isinstance(meta[-1], BaseChannel):
|
||||
return meta[-1]
|
||||
elif len(meta) >= 1 and isclass(meta[-1]) and issubclass(meta[-1], BaseChannel):
|
||||
return meta[-1](typ.__origin__ if hasattr(typ, "__origin__") else typ)
|
||||
# Search through all annotated medata to find channel annotations
|
||||
for item in meta:
|
||||
if isinstance(item, BaseChannel):
|
||||
return item
|
||||
elif isclass(item) and issubclass(item, BaseChannel):
|
||||
# ex, Annotated[int, EphemeralValue, SomeOtherAnnotation]
|
||||
# would return EphemeralValue(int)
|
||||
return item(typ.__origin__ if hasattr(typ, "__origin__") else typ)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -82,6 +82,8 @@ def push_ui_message(
|
||||
message: Optional message object to associate with the UI message.
|
||||
state_key: Key in the graph state where the UI messages are stored.
|
||||
Defaults to "ui".
|
||||
merge: Whether to merge props with existing UI message (True) or replace
|
||||
them (False). Defaults to False.
|
||||
|
||||
Returns:
|
||||
The created UI message.
|
||||
@@ -186,7 +188,7 @@ def ui_message_reducer(
|
||||
|
||||
messages = ui_message_reducer(
|
||||
[{"type": "ui", "id": "1", "name": "Chat", "props": {}}],
|
||||
{"type": "remove-ui", "id": "1"}
|
||||
{"type": "remove-ui", "id": "1"},
|
||||
)
|
||||
|
||||
"""
|
||||
|
||||
@@ -23,6 +23,14 @@ from typing import (
|
||||
from langchain_core.callbacks import Callbacks
|
||||
from langchain_core.callbacks.manager import AsyncParentRunManager, ParentRunManager
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
from langgraph.checkpoint.base import (
|
||||
BaseCheckpointSaver,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
PendingWrite,
|
||||
V,
|
||||
)
|
||||
from langgraph.store.base import BaseStore
|
||||
from xxhash import xxh3_128_hexdigest
|
||||
|
||||
from langgraph._internal._config import merge_configs, patch_config
|
||||
@@ -57,13 +65,6 @@ from langgraph._internal._scratchpad import PregelScratchpad
|
||||
from langgraph._internal._typing import EMPTY_SEQ, MISSING
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.channels.topic import Topic
|
||||
from langgraph.checkpoint.base import (
|
||||
BaseCheckpointSaver,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
PendingWrite,
|
||||
V,
|
||||
)
|
||||
from langgraph.constants import TAG_HIDDEN
|
||||
from langgraph.managed.base import ManagedValueMapping
|
||||
from langgraph.pregel._call import get_runnable_for_task, identifier
|
||||
@@ -71,7 +72,6 @@ from langgraph.pregel._io import read_channels
|
||||
from langgraph.pregel._log import logger
|
||||
from langgraph.pregel._read import INPUT_CACHE_KEY_TYPE, PregelNode
|
||||
from langgraph.runtime import DEFAULT_RUNTIME, Runtime
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import (
|
||||
All,
|
||||
CacheKey,
|
||||
@@ -715,13 +715,17 @@ def prepare_single_task(
|
||||
runtime = runtime.override(
|
||||
store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
|
||||
)
|
||||
additional_config: RunnableConfig = {
|
||||
"metadata": metadata,
|
||||
"tags": proc.tags,
|
||||
}
|
||||
return PregelExecutableTask(
|
||||
packet.node,
|
||||
packet.arg,
|
||||
proc_node,
|
||||
writes,
|
||||
patch_config(
|
||||
merge_configs(config, {"metadata": metadata, "tags": proc.tags}),
|
||||
merge_configs(config, additional_config),
|
||||
run_name=packet.node,
|
||||
callbacks=(
|
||||
manager.get_child(f"graph:step:{step}") if manager else None
|
||||
@@ -856,15 +860,17 @@ def prepare_single_task(
|
||||
previous=checkpoint["channel_values"].get(PREVIOUS, None),
|
||||
store=store,
|
||||
)
|
||||
additional_config = {
|
||||
"metadata": metadata,
|
||||
"tags": proc.tags,
|
||||
}
|
||||
return PregelExecutableTask(
|
||||
name,
|
||||
val,
|
||||
node,
|
||||
writes,
|
||||
patch_config(
|
||||
merge_configs(
|
||||
config, {"metadata": metadata, "tags": proc.tags}
|
||||
),
|
||||
merge_configs(config, additional_config),
|
||||
run_name=name,
|
||||
callbacks=(
|
||||
manager.get_child(f"graph:step:{step}")
|
||||
|
||||
@@ -7,7 +7,7 @@ import functools
|
||||
import inspect
|
||||
import sys
|
||||
import types
|
||||
from collections.abc import Generator, Sequence
|
||||
from collections.abc import Awaitable, Generator, Sequence
|
||||
from typing import Any, Callable, Generic, TypeVar, cast
|
||||
|
||||
from langchain_core.runnables import Runnable
|
||||
@@ -251,7 +251,7 @@ class SyncAsyncFuture(Generic[T], concurrent.futures.Future[T]):
|
||||
|
||||
|
||||
def call(
|
||||
func: Callable[P, T],
|
||||
func: Callable[P, Awaitable[T]] | Callable[P, T],
|
||||
*args: Any,
|
||||
retry_policy: Sequence[RetryPolicy] | None = None,
|
||||
cache_policy: CachePolicy | None = None,
|
||||
|
||||
@@ -3,10 +3,11 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
from langgraph.checkpoint.base.id import uuid6
|
||||
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.managed.base import ManagedValueMapping, ManagedValueSpec
|
||||
|
||||
LATEST_VERSION = 4
|
||||
|
||||
@@ -2,14 +2,15 @@ from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, cast
|
||||
from typing import Any, NamedTuple, cast
|
||||
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
from langchain_core.runnables.graph import Graph, Node
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
|
||||
from langgraph._internal._constants import CONF, CONFIG_KEY_SEND, INPUT
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.channels.last_value import LastValueAfterFinish
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.managed.base import ManagedValueSpec
|
||||
from langgraph.pregel._algo import (
|
||||
@@ -25,6 +26,19 @@ from langgraph.pregel._write import ChannelWrite
|
||||
from langgraph.types import All, Checkpointer
|
||||
|
||||
|
||||
class Edge(NamedTuple):
|
||||
source: str
|
||||
target: str
|
||||
conditional: bool
|
||||
data: str | None
|
||||
|
||||
|
||||
class TriggerEdge(NamedTuple):
|
||||
source: str
|
||||
conditional: bool
|
||||
data: str | None
|
||||
|
||||
|
||||
def draw_graph(
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
@@ -49,7 +63,7 @@ def draw_graph(
|
||||
The graph for this Pregel instance.
|
||||
"""
|
||||
# (src, dest, is_conditional, label)
|
||||
edges: set[tuple[str, str, bool, str | None]] = set()
|
||||
edges: set[Edge] = set()
|
||||
|
||||
step = -1
|
||||
checkpoint = empty_checkpoint()
|
||||
@@ -63,8 +77,9 @@ def draw_graph(
|
||||
checkpoint,
|
||||
)
|
||||
static_seen: set[Any] = set()
|
||||
sources: dict[str, set[tuple[str, bool, str | None]]] = {}
|
||||
step_sources: dict[str, set[tuple[str, bool, str | None]]] = {}
|
||||
sources: dict[str, set[TriggerEdge]] = {}
|
||||
step_sources: dict[str, set[TriggerEdge]] = {}
|
||||
static_declared_writes: dict[str, set[TriggerEdge]] = defaultdict(set)
|
||||
# remove node mappers
|
||||
nodes = {
|
||||
k: v.copy(update={"mapper": None}) if v.mapper is not None else v
|
||||
@@ -123,32 +138,36 @@ def draw_graph(
|
||||
# END writes are not written, but become edges directly
|
||||
for t in writes:
|
||||
if t[0] == END:
|
||||
edges.add((task.name, t[0], True, t[2]))
|
||||
edges.add(Edge(task.name, t[0], True, t[2]))
|
||||
writes = [t for t in writes if t[0] != END]
|
||||
conditionals.update(
|
||||
{(task.name, t[0], t[1] or None): t[2] for t in writes}
|
||||
)
|
||||
# record static writes for edge creation
|
||||
for t in writes:
|
||||
static_declared_writes[task.name].add(
|
||||
TriggerEdge(t[0], True, t[2])
|
||||
)
|
||||
task.config[CONF][CONFIG_KEY_SEND]([t[:2] for t in writes])
|
||||
# collect sources
|
||||
step_sources = {
|
||||
task.name: {
|
||||
(
|
||||
step_sources = {}
|
||||
for task in tasks.values():
|
||||
task_edges = {
|
||||
TriggerEdge(
|
||||
w[0],
|
||||
(task.name, w[0], w[1] or None) in conditionals,
|
||||
conditionals.get((task.name, w[0], w[1] or None)),
|
||||
)
|
||||
for w in task.writes
|
||||
}
|
||||
for task in tasks.values()
|
||||
}
|
||||
task_edges |= static_declared_writes.get(task.name, set())
|
||||
step_sources[task.name] = task_edges
|
||||
sources.update(step_sources)
|
||||
# invert triggers
|
||||
trigger_to_sources: dict[str, set[tuple[str, bool, str | None]]] = defaultdict(
|
||||
set
|
||||
)
|
||||
trigger_to_sources: dict[str, set[TriggerEdge]] = defaultdict(set)
|
||||
for src, triggers in sources.items():
|
||||
for trigger, cond, label in triggers:
|
||||
trigger_to_sources[trigger].add((src, cond, label))
|
||||
trigger_to_sources[trigger].add(TriggerEdge(src, cond, label))
|
||||
# apply writes
|
||||
updated_channels = apply_writes(
|
||||
checkpoint, channels, tasks.values(), get_next_version, trigger_to_nodes
|
||||
@@ -170,26 +189,39 @@ def draw_graph(
|
||||
trigger_to_nodes=trigger_to_nodes,
|
||||
updated_channels=updated_channels,
|
||||
)
|
||||
# collect deferred nodes
|
||||
deferred_nodes: set[str] = set()
|
||||
edges_to_deferred_nodes: set[Edge] = set()
|
||||
for channel, item in channels.items():
|
||||
if isinstance(item, LastValueAfterFinish):
|
||||
deferred_node = channel.split(":", 2)[-1]
|
||||
deferred_nodes.add(deferred_node)
|
||||
# collect edges
|
||||
for task in tasks.values():
|
||||
added = False
|
||||
for trigger in task.triggers:
|
||||
for src, cond, label in sorted(trigger_to_sources[trigger]):
|
||||
edges.add((src, task.name, cond, label))
|
||||
# record edge to be reviewed later
|
||||
if task.name in deferred_nodes:
|
||||
edges_to_deferred_nodes.add(Edge(src, task.name, cond, label))
|
||||
edges.add(Edge(src, task.name, cond, label))
|
||||
# if the edge is from this step, skip adding the implicit edges
|
||||
if (trigger, cond, label) in step_sources.get(src, set()):
|
||||
added = True
|
||||
else:
|
||||
sources[src].discard((trigger, cond, label))
|
||||
sources[src].discard(TriggerEdge(trigger, cond, label))
|
||||
# if no edges from this step, add implicit edges from all previous tasks
|
||||
if not added:
|
||||
for src in step_sources:
|
||||
edges.add((src, task.name, True, None))
|
||||
edges.add(Edge(src, task.name, True, None))
|
||||
|
||||
# assemble the graph
|
||||
graph = Graph()
|
||||
# add nodes
|
||||
for name, node in nodes.items():
|
||||
metadata = dict(node.metadata or {})
|
||||
if name in deferred_nodes:
|
||||
metadata["defer"] = True
|
||||
if name in interrupt_before_nodes and name in interrupt_after_nodes:
|
||||
metadata["__interrupt"] = "before,after"
|
||||
elif name in interrupt_before_nodes:
|
||||
@@ -215,10 +247,11 @@ def draw_graph(
|
||||
termini = {d for _, d, _, _ in edges if d != END}.difference(
|
||||
s for s, _, _, _ in edges
|
||||
)
|
||||
end_edge_exists = any(d == END for _, d, _, _ in edges)
|
||||
if termini:
|
||||
for src in sorted(termini):
|
||||
add_edge(graph, src, END)
|
||||
elif len(step_sources) == 1:
|
||||
elif len(step_sources) == 1 and not end_edge_exists:
|
||||
for src in sorted(step_sources):
|
||||
add_edge(graph, src, END, conditional=True)
|
||||
# replace subgraphs
|
||||
|
||||
@@ -25,6 +25,17 @@ from typing import (
|
||||
|
||||
from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
PendingWrite,
|
||||
)
|
||||
from langgraph.store.base import BaseStore
|
||||
from typing_extensions import ParamSpec, Self
|
||||
|
||||
from langgraph._internal._config import patch_configurable
|
||||
@@ -50,17 +61,7 @@ from langgraph._internal._constants import (
|
||||
)
|
||||
from langgraph._internal._scratchpad import PregelScratchpad
|
||||
from langgraph._internal._typing import EMPTY_SEQ, MISSING
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
PendingWrite,
|
||||
)
|
||||
from langgraph.constants import TAG_HIDDEN
|
||||
from langgraph.errors import (
|
||||
EmptyInputError,
|
||||
@@ -108,7 +109,6 @@ from langgraph.pregel.debug import (
|
||||
map_debug_tasks,
|
||||
)
|
||||
from langgraph.pregel.protocol import StreamChunk, StreamProtocol
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import (
|
||||
All,
|
||||
CachePolicy,
|
||||
@@ -242,7 +242,9 @@ class PregelLoop:
|
||||
self.interrupt_before = interrupt_before
|
||||
self.manager = manager
|
||||
self.is_nested = CONFIG_KEY_TASK_ID in self.config.get(CONF, {})
|
||||
self.skip_done_tasks = CONFIG_KEY_CHECKPOINT_ID not in config[CONF]
|
||||
self.skip_done_tasks = CONFIG_KEY_CHECKPOINT_ID not in config[CONF] or (
|
||||
CONFIG_KEY_RESUMING in self.config[CONF] and self.is_nested
|
||||
)
|
||||
self._migrate_checkpoint = migrate_checkpoint
|
||||
self.trigger_to_nodes = trigger_to_nodes
|
||||
self.retry_policy = retry_policy
|
||||
@@ -568,6 +570,36 @@ class PregelLoop:
|
||||
if task := tasks.get(tid):
|
||||
task.writes.append((k, v))
|
||||
|
||||
def _pending_interrupts(self) -> set[str]:
|
||||
"""Return the set of interrupt ids that are pending without corresponding resume values."""
|
||||
# mapping of task ids to interrupt ids
|
||||
pending_interrupts: dict[str, str] = {}
|
||||
|
||||
# set of resume task ids
|
||||
pending_resumes: set[str] = set()
|
||||
|
||||
for task_id, write_type, value in self.checkpoint_pending_writes:
|
||||
if write_type == INTERRUPT:
|
||||
# interrupts is always a list, but there should only be one element
|
||||
pending_interrupts[task_id] = value[0].id
|
||||
elif write_type == RESUME:
|
||||
pending_resumes.add(task_id)
|
||||
|
||||
resumed_interrupt_ids = {
|
||||
pending_interrupts[task_id]
|
||||
for task_id in pending_resumes
|
||||
if task_id in pending_interrupts
|
||||
}
|
||||
|
||||
# Keep only interrupts whose interrupt_id is not resumed
|
||||
hanging_interrupts: set[str] = {
|
||||
interrupt_id
|
||||
for interrupt_id in pending_interrupts.values()
|
||||
if interrupt_id not in resumed_interrupt_ids
|
||||
}
|
||||
|
||||
return hanging_interrupts
|
||||
|
||||
def _first(
|
||||
self, *, input_keys: str | Sequence[str], updated_channels: set[str] | None
|
||||
) -> set[str] | None:
|
||||
@@ -590,16 +622,24 @@ class PregelLoop:
|
||||
|
||||
# map command to writes
|
||||
if isinstance(self.input, Command):
|
||||
if resume_is_map := (
|
||||
(resume := self.input.resume) is not None
|
||||
and isinstance(resume, dict)
|
||||
and all(is_xxh3_128_hexdigest(k) for k in resume)
|
||||
):
|
||||
self.config[CONF][CONFIG_KEY_RESUME_MAP] = self.input.resume
|
||||
if resume is not None and not self.checkpointer:
|
||||
raise RuntimeError(
|
||||
"Cannot use Command(resume=...) without checkpointer"
|
||||
)
|
||||
if (resume := self.input.resume) is not None:
|
||||
if not self.checkpointer:
|
||||
raise RuntimeError(
|
||||
"Cannot use Command(resume=...) without checkpointer"
|
||||
)
|
||||
|
||||
if resume_is_map := (
|
||||
isinstance(resume, dict)
|
||||
and all(is_xxh3_128_hexdigest(k) for k in resume)
|
||||
):
|
||||
self.config[CONF][CONFIG_KEY_RESUME_MAP] = resume
|
||||
else:
|
||||
if len(self._pending_interrupts()) > 1:
|
||||
raise RuntimeError(
|
||||
"When there are multiple pending interrupts, you must specify the interrupt id when resuming. "
|
||||
"Docs: https://docs.langchain.com/oss/python/langgraph/add-human-in-the-loop#resume-multiple-interrupts-with-one-invocation."
|
||||
)
|
||||
|
||||
writes: defaultdict[str, list[tuple[str, Any]]] = defaultdict(list)
|
||||
# group writes by task ID
|
||||
for tid, c, v in map_command(cmd=self.input):
|
||||
@@ -866,7 +906,11 @@ class PregelLoop:
|
||||
)
|
||||
}
|
||||
]
|
||||
self._emit("updates", lambda: iter(interrupts))
|
||||
stream_modes = self.stream.modes if self.stream else []
|
||||
if "updates" in stream_modes:
|
||||
self._emit("updates", lambda: iter(interrupts))
|
||||
elif "values" in stream_modes:
|
||||
self._emit("values", lambda: iter(interrupts))
|
||||
elif writes[0][0] != ERROR:
|
||||
self._emit(
|
||||
"updates",
|
||||
|
||||
@@ -231,9 +231,10 @@ class PregelNode:
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any | None,
|
||||
) -> Any:
|
||||
self_config: RunnableConfig = {"metadata": self.metadata, "tags": self.tags}
|
||||
return self.bound.invoke(
|
||||
input,
|
||||
merge_configs({"metadata": self.metadata, "tags": self.tags}, config),
|
||||
merge_configs(self_config, config),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -243,9 +244,10 @@ class PregelNode:
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any | None,
|
||||
) -> Any:
|
||||
self_config: RunnableConfig = {"metadata": self.metadata, "tags": self.tags}
|
||||
return await self.bound.ainvoke(
|
||||
input,
|
||||
merge_configs({"metadata": self.metadata, "tags": self.tags}, config),
|
||||
merge_configs(self_config, config),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -255,9 +257,10 @@ class PregelNode:
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any | None,
|
||||
) -> Iterator[Any]:
|
||||
self_config: RunnableConfig = {"metadata": self.metadata, "tags": self.tags}
|
||||
yield from self.bound.stream(
|
||||
input,
|
||||
merge_configs({"metadata": self.metadata, "tags": self.tags}, config),
|
||||
merge_configs(self_config, config),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -267,9 +270,10 @@ class PregelNode:
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any | None,
|
||||
) -> AsyncIterator[Any]:
|
||||
self_config: RunnableConfig = {"metadata": self.metadata, "tags": self.tags}
|
||||
async for item in self.bound.astream(
|
||||
input,
|
||||
merge_configs({"metadata": self.metadata, "tags": self.tags}, config),
|
||||
merge_configs(self_config, config),
|
||||
**kwargs,
|
||||
):
|
||||
yield item
|
||||
@@ -7,10 +7,10 @@ import textwrap
|
||||
from typing import Any, Callable
|
||||
|
||||
from langchain_core.runnables import Runnable, RunnableLambda, RunnableSequence
|
||||
from langgraph.checkpoint.base import ChannelVersions
|
||||
from typing_extensions import override
|
||||
|
||||
from langgraph._internal._runnable import RunnableCallable, RunnableSeq
|
||||
from langgraph.checkpoint.base import ChannelVersions
|
||||
from langgraph.pregel.protocol import PregelProtocol
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.checkpoint.base import CheckpointMetadata, PendingWrite
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph._internal._config import patch_checkpoint_map
|
||||
@@ -20,7 +21,6 @@ from langgraph._internal._constants import (
|
||||
)
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import CheckpointMetadata, PendingWrite
|
||||
from langgraph.constants import TAG_HIDDEN
|
||||
from langgraph.pregel._io import read_channels
|
||||
from langgraph.types import PregelExecutableTask, PregelTask, StateSnapshot
|
||||
@@ -48,7 +48,7 @@ class CheckpointTask(TypedDict):
|
||||
name: str
|
||||
error: str | None
|
||||
interrupts: list[dict]
|
||||
state: RunnableConfig | None
|
||||
state: StateSnapshot | RunnableConfig | None
|
||||
|
||||
|
||||
class CheckpointPayload(TypedDict):
|
||||
|
||||
@@ -3,15 +3,24 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import concurrent
|
||||
import concurrent.futures
|
||||
import contextlib
|
||||
import queue
|
||||
import warnings
|
||||
import weakref
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
|
||||
from collections.abc import AsyncIterator, Awaitable, Iterator, Mapping, Sequence
|
||||
from dataclasses import is_dataclass
|
||||
from functools import partial
|
||||
from inspect import isclass
|
||||
from typing import Any, Callable, Generic, Optional, Union, cast, get_type_hints
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Optional,
|
||||
Union,
|
||||
cast,
|
||||
get_type_hints,
|
||||
)
|
||||
from uuid import UUID, uuid5
|
||||
|
||||
from langchain_core.globals import get_debug
|
||||
@@ -25,6 +34,13 @@ from langchain_core.runnables.config import (
|
||||
get_callback_manager_for_config,
|
||||
)
|
||||
from langchain_core.runnables.graph import Graph
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.checkpoint.base import (
|
||||
BaseCheckpointSaver,
|
||||
Checkpoint,
|
||||
CheckpointTuple,
|
||||
)
|
||||
from langgraph.store.base import BaseStore
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
from typing_extensions import Self, Unpack, deprecated, is_typeddict
|
||||
|
||||
@@ -73,14 +89,8 @@ from langgraph._internal._runnable import (
|
||||
coerce_to_runnable,
|
||||
)
|
||||
from langgraph._internal._typing import MISSING, DeprecatedKwargs
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.channels.topic import Topic
|
||||
from langgraph.checkpoint.base import (
|
||||
BaseCheckpointSaver,
|
||||
Checkpoint,
|
||||
CheckpointTuple,
|
||||
)
|
||||
from langgraph.config import get_config
|
||||
from langgraph.constants import END
|
||||
from langgraph.errors import (
|
||||
@@ -117,7 +127,6 @@ from langgraph.pregel._write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.pregel.debug import get_bolded_text, get_colored_text, tasks_w_writes
|
||||
from langgraph.pregel.protocol import PregelProtocol, StreamChunk, StreamProtocol
|
||||
from langgraph.runtime import DEFAULT_RUNTIME, Runtime
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import (
|
||||
All,
|
||||
CachePolicy,
|
||||
@@ -377,7 +386,7 @@ class Pregel(
|
||||
|
||||
However, for **advanced** use cases, Pregel can be used directly. If you're
|
||||
not sure whether you need to use Pregel directly, then the answer is probably no
|
||||
– you should use the Graph API or Functional API instead. These are higher-level
|
||||
- you should use the Graph API or Functional API instead. These are higher-level
|
||||
interfaces that will compile down to Pregel under the hood.
|
||||
|
||||
Here are some examples to give you a sense of how it works:
|
||||
@@ -479,7 +488,7 @@ class Pregel(
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'c': ['foofoo', 'foofoofoofoo']}
|
||||
{"c": ["foofoo", "foofoofoofoo"]}
|
||||
```
|
||||
|
||||
Example: Using a BinaryOperatorAggregate channel
|
||||
@@ -507,6 +516,7 @@ class Pregel(
|
||||
else:
|
||||
return update
|
||||
|
||||
|
||||
app = Pregel(
|
||||
nodes={"node1": node1, "node2": node2},
|
||||
channels={
|
||||
@@ -515,7 +525,7 @@ class Pregel(
|
||||
"c": BinaryOperatorAggregate(str, operator=reducer),
|
||||
},
|
||||
input_channels=["a"],
|
||||
output_channels=["c"]
|
||||
output_channels=["c"],
|
||||
)
|
||||
|
||||
app.invoke({"a": "foo"})
|
||||
@@ -535,7 +545,8 @@ class Pregel(
|
||||
from langgraph.pregel import Pregel, NodeBuilder, ChannelWriteEntry
|
||||
|
||||
example_node = (
|
||||
NodeBuilder().subscribe_only("value")
|
||||
NodeBuilder()
|
||||
.subscribe_only("value")
|
||||
.do(lambda x: x + x if len(x) < 10 else None)
|
||||
.write_to(ChannelWriteEntry(channel="value", skip_none=True))
|
||||
)
|
||||
@@ -546,7 +557,7 @@ class Pregel(
|
||||
"value": EphemeralValue(str),
|
||||
},
|
||||
input_channels=["value"],
|
||||
output_channels=["value"]
|
||||
output_channels=["value"],
|
||||
)
|
||||
|
||||
app.invoke({"value": "a"})
|
||||
@@ -2612,6 +2623,7 @@ class Pregel(
|
||||
if subgraphs:
|
||||
loop.config[CONF][CONFIG_KEY_STREAM] = loop.stream
|
||||
# enable concurrent streaming
|
||||
get_waiter: Callable[[], concurrent.futures.Future[None]] | None = None
|
||||
if (
|
||||
self.stream_eager
|
||||
or subgraphs
|
||||
@@ -2634,8 +2646,6 @@ class Pregel(
|
||||
else:
|
||||
return waiter
|
||||
|
||||
else:
|
||||
get_waiter = None # type: ignore[assignment]
|
||||
# Similarly to Bulk Synchronous Parallel / Pregel model
|
||||
# computation proceeds in steps, while there are channel updates.
|
||||
# Channel updates from step N are only visible in step N+1
|
||||
@@ -2916,45 +2926,77 @@ class Pregel(
|
||||
stream_put, stream_modes
|
||||
)
|
||||
# enable concurrent streaming
|
||||
get_waiter: Callable[[], asyncio.Task[None]] | None = None
|
||||
_cleanup_waiter: Callable[[], Awaitable[None]] | None = None
|
||||
if (
|
||||
self.stream_eager
|
||||
or subgraphs
|
||||
or "messages" in stream_modes
|
||||
or "custom" in stream_modes
|
||||
):
|
||||
# Keep a single waiter task alive; ensure cleanup on exit.
|
||||
waiter: asyncio.Task[None] | None = None
|
||||
|
||||
def get_waiter() -> asyncio.Task[None]:
|
||||
return aioloop.create_task(stream.wait())
|
||||
nonlocal waiter
|
||||
if waiter is None or waiter.done():
|
||||
waiter = aioloop.create_task(stream.wait())
|
||||
|
||||
def _clear(t: asyncio.Task[None]) -> None:
|
||||
nonlocal waiter
|
||||
if waiter is t:
|
||||
waiter = None
|
||||
|
||||
waiter.add_done_callback(_clear)
|
||||
return waiter
|
||||
|
||||
async def _cleanup_waiter() -> None:
|
||||
"""Wake pending waiter and/or cancel+await to avoid pending tasks."""
|
||||
nonlocal waiter
|
||||
# Try to wake via semaphore like SyncPregelLoop
|
||||
with contextlib.suppress(Exception):
|
||||
if hasattr(stream, "_count"):
|
||||
stream._count.release()
|
||||
t = waiter
|
||||
waiter = None
|
||||
if t is not None and not t.done():
|
||||
t.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await t
|
||||
|
||||
else:
|
||||
get_waiter = None # type: ignore[assignment]
|
||||
# Similarly to Bulk Synchronous Parallel / Pregel model
|
||||
# computation proceeds in steps, while there are channel updates
|
||||
# channel updates from step N are only visible in step N+1
|
||||
# channels are guaranteed to be immutable for the duration of the step,
|
||||
# with channel updates applied only at the transition between steps
|
||||
while loop.tick():
|
||||
for task in await loop.amatch_cached_writes():
|
||||
loop.output_writes(task.id, task.writes, cached=True)
|
||||
async for _ in runner.atick(
|
||||
[t for t in loop.tasks.values() if not t.writes],
|
||||
timeout=self.step_timeout,
|
||||
get_waiter=get_waiter,
|
||||
schedule_task=loop.aaccept_push,
|
||||
):
|
||||
# emit output
|
||||
for o in _output(
|
||||
stream_mode,
|
||||
print_mode,
|
||||
subgraphs,
|
||||
stream.get_nowait,
|
||||
asyncio.QueueEmpty,
|
||||
try:
|
||||
while loop.tick():
|
||||
for task in await loop.amatch_cached_writes():
|
||||
loop.output_writes(task.id, task.writes, cached=True)
|
||||
async for _ in runner.atick(
|
||||
[t for t in loop.tasks.values() if not t.writes],
|
||||
timeout=self.step_timeout,
|
||||
get_waiter=get_waiter,
|
||||
schedule_task=loop.aaccept_push,
|
||||
):
|
||||
yield o
|
||||
loop.after_tick()
|
||||
# wait for checkpoint
|
||||
if durability_ == "sync":
|
||||
await cast(asyncio.Future, loop._put_checkpoint_fut)
|
||||
# emit output
|
||||
for o in _output(
|
||||
stream_mode,
|
||||
print_mode,
|
||||
subgraphs,
|
||||
stream.get_nowait,
|
||||
asyncio.QueueEmpty,
|
||||
):
|
||||
yield o
|
||||
loop.after_tick()
|
||||
# wait for checkpoint
|
||||
if durability_ == "sync":
|
||||
await cast(asyncio.Future, loop._put_checkpoint_fut)
|
||||
finally:
|
||||
# ensure waiter doesn't remain pending on cancel/shutdown
|
||||
if _cleanup_waiter is not None:
|
||||
await _cleanup_waiter()
|
||||
|
||||
# emit output
|
||||
for o in _output(
|
||||
stream_mode,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
from dataclasses import asdict
|
||||
from typing import (
|
||||
@@ -7,6 +8,7 @@ from typing import (
|
||||
Literal,
|
||||
cast,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
import langsmith as ls
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
@@ -19,6 +21,7 @@ from langchain_core.runnables.graph import (
|
||||
from langchain_core.runnables.graph import (
|
||||
Node as DrawableNode,
|
||||
)
|
||||
from langgraph.checkpoint.base import CheckpointMetadata
|
||||
from langgraph_sdk.client import (
|
||||
LangGraphClient,
|
||||
SyncLangGraphClient,
|
||||
@@ -49,7 +52,6 @@ from langgraph._internal._constants import (
|
||||
INTERRUPT,
|
||||
NS_SEP,
|
||||
)
|
||||
from langgraph.checkpoint.base import CheckpointMetadata
|
||||
from langgraph.errors import GraphInterrupt, ParentCommand
|
||||
from langgraph.pregel.protocol import PregelProtocol, StreamProtocol
|
||||
from langgraph.types import (
|
||||
@@ -61,6 +63,8 @@ from langgraph.types import (
|
||||
StreamMode,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ("RemoteGraph", "RemoteException")
|
||||
|
||||
_CONF_DROPLIST = frozenset(
|
||||
@@ -75,7 +79,7 @@ _CONF_DROPLIST = frozenset(
|
||||
|
||||
def _sanitize_config_value(v: Any) -> Any:
|
||||
"""Recursively sanitize a config value to ensure it contains only primitives."""
|
||||
if isinstance(v, (str, int, float, bool)):
|
||||
if isinstance(v, (str, int, float, bool, UUID)):
|
||||
return v
|
||||
elif isinstance(v, dict):
|
||||
sanitized_dict = {}
|
||||
@@ -950,6 +954,7 @@ class RemoteGraph(PregelProtocol):
|
||||
try:
|
||||
return chunk
|
||||
except UnboundLocalError:
|
||||
logger.warning("No events received from remote graph")
|
||||
return None
|
||||
|
||||
async def ainvoke(
|
||||
@@ -990,6 +995,7 @@ class RemoteGraph(PregelProtocol):
|
||||
try:
|
||||
return chunk
|
||||
except UnboundLocalError:
|
||||
logger.warning("No events received from remote graph")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field, replace
|
||||
from typing import Any, Generic, cast
|
||||
|
||||
from langgraph.store.base import BaseStore
|
||||
from typing_extensions import TypedDict, Unpack
|
||||
|
||||
from langgraph._internal._constants import CONF, CONFIG_KEY_RUNTIME
|
||||
from langgraph.config import get_config
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import _DC_KWARGS, StreamWriter
|
||||
from langgraph.typing import ContextT
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from typing import (
|
||||
from warnings import warn
|
||||
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata
|
||||
from typing_extensions import Unpack, deprecated
|
||||
from xxhash import xxh3_128_hexdigest
|
||||
|
||||
@@ -26,7 +27,6 @@ from langgraph._internal._cache import default_cache_key
|
||||
from langgraph._internal._fields import get_cached_annotated_keys, get_update_as_tuples
|
||||
from langgraph._internal._retry import default_retry_on
|
||||
from langgraph._internal._typing import MISSING, DeprecatedKwargs
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata
|
||||
from langgraph.warnings import LangGraphDeprecatedSinceV10
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -296,12 +296,10 @@ class Send:
|
||||
>>> class OverallState(TypedDict):
|
||||
... subjects: list[str]
|
||||
... jokes: Annotated[list[str], operator.add]
|
||||
...
|
||||
>>> from langgraph.types import Send
|
||||
>>> from langgraph.graph import END, START
|
||||
>>> def continue_to_jokes(state: OverallState):
|
||||
... return [Send("generate_joke", {"subject": s}) for s in state['subjects']]
|
||||
...
|
||||
... return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
|
||||
>>> from langgraph.graph import StateGraph
|
||||
>>> builder = StateGraph(OverallState)
|
||||
>>> builder.add_node("generate_joke", lambda state: {"jokes": [f"Joke about {state['subject']}"]})
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph"
|
||||
version = "0.6.7"
|
||||
version = "0.6.8"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
requires-python = ">=3.9"
|
||||
@@ -74,14 +74,6 @@ target-version = "py39"
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/bench/*" = ["UP006", "UP007"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
skip-magic-trailing-comma = false
|
||||
line-ending = "auto"
|
||||
docstring-code-format = false
|
||||
docstring-code-line-length = "dynamic"
|
||||
|
||||
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
||||
"typing.TypedDict".msg = "Use typing_extensions.TypedDict instead."
|
||||
|
||||
|
||||
@@ -88,10 +88,10 @@
|
||||
'''
|
||||
# ---
|
||||
# name: test_message_graph[memory]
|
||||
'{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the toLine truncated
|
||||
'{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the toLine truncated
|
||||
# ---
|
||||
# name: test_message_graph[memory].1
|
||||
'{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the toLine truncated
|
||||
'{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "example": {"default": false, "title": "Example", "type": "boolean"}, "tool_calls": {"default": [], "items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"default": [], "items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"default": [], "items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the toLine truncated
|
||||
# ---
|
||||
# name: test_message_graph[memory].2
|
||||
'''
|
||||
|
||||
@@ -307,6 +307,99 @@
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_get_graph_nonterminal_last_step_source
|
||||
'''
|
||||
{
|
||||
"edges": [
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": "human"
|
||||
},
|
||||
{
|
||||
"conditional": true,
|
||||
"source": "chatbot",
|
||||
"target": "human"
|
||||
},
|
||||
{
|
||||
"conditional": true,
|
||||
"source": "chatbot",
|
||||
"target": "tools"
|
||||
},
|
||||
{
|
||||
"conditional": true,
|
||||
"source": "human",
|
||||
"target": "__end__"
|
||||
},
|
||||
{
|
||||
"conditional": true,
|
||||
"source": "human",
|
||||
"target": "chatbot"
|
||||
},
|
||||
{
|
||||
"source": "tools",
|
||||
"target": "chatbot"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"id": [
|
||||
"langgraph",
|
||||
"_internal",
|
||||
"_runnable",
|
||||
"RunnableCallable"
|
||||
],
|
||||
"name": "__start__"
|
||||
},
|
||||
"id": "__start__",
|
||||
"type": "runnable"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"id": [
|
||||
"langgraph",
|
||||
"_internal",
|
||||
"_runnable",
|
||||
"RunnableCallable"
|
||||
],
|
||||
"name": "chatbot"
|
||||
},
|
||||
"id": "chatbot",
|
||||
"type": "runnable"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"id": [
|
||||
"langgraph",
|
||||
"_internal",
|
||||
"_runnable",
|
||||
"RunnableCallable"
|
||||
],
|
||||
"name": "tools"
|
||||
},
|
||||
"id": "tools",
|
||||
"type": "runnable"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"id": [
|
||||
"langgraph",
|
||||
"_internal",
|
||||
"_runnable",
|
||||
"RunnableCallable"
|
||||
],
|
||||
"name": "human"
|
||||
},
|
||||
"id": "human",
|
||||
"type": "runnable"
|
||||
},
|
||||
{
|
||||
"id": "__end__"
|
||||
}
|
||||
]
|
||||
}
|
||||
'''
|
||||
# ---
|
||||
# name: test_get_graph_root_channel
|
||||
'''
|
||||
{
|
||||
|
||||
@@ -4,14 +4,14 @@ from uuid import UUID
|
||||
|
||||
import pytest
|
||||
import redis
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from langgraph.cache.base import BaseCache
|
||||
from langgraph.cache.memory import InMemoryCache
|
||||
from langgraph.cache.redis import RedisCache
|
||||
from langgraph.cache.sqlite import SqliteCache
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.store.base import BaseStore
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from langgraph.types import Durability
|
||||
from tests.conftest_checkpointer import (
|
||||
_checkpointer_memory,
|
||||
|
||||
@@ -3,14 +3,13 @@ from contextlib import asynccontextmanager, contextmanager
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from psycopg import AsyncConnection, Connection
|
||||
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.postgres import PostgresSaver
|
||||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
from psycopg import AsyncConnection, Connection
|
||||
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
||||
|
||||
pytest.register_assert_rewrite("tests.memory_assert")
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@ from contextlib import asynccontextmanager, contextmanager
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from psycopg import AsyncConnection, Connection
|
||||
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.store.postgres import AsyncPostgresStore, PostgresStore
|
||||
from psycopg import AsyncConnection, Connection
|
||||
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.json",
|
||||
"$schema": "https://langgra.ph/schema.json",
|
||||
"graphs": {
|
||||
"app": "tests/example_app/example_graph.py:app"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ from functools import partial
|
||||
from typing import Any, Optional
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
BaseCheckpointSaver,
|
||||
ChannelVersions,
|
||||
@@ -15,6 +14,7 @@ from langgraph.checkpoint.base import (
|
||||
SerializerProtocol,
|
||||
)
|
||||
from langgraph.checkpoint.memory import InMemorySaver, PersistentDict
|
||||
|
||||
from langgraph.constants import TASKS
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ from collections import defaultdict
|
||||
from typing import Annotated, Literal, Optional, Union
|
||||
|
||||
import pytest
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointTuple
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph._internal._config import patch_configurable
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointTuple
|
||||
from langgraph.graph.state import StateGraph
|
||||
from langgraph.pregel._checkpoint import copy_checkpoint
|
||||
from langgraph.types import Command, Interrupt, PregelTask, StateSnapshot, interrupt
|
||||
|
||||
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user