Compare commits

..
Author SHA1 Message Date
Sam Crowder be0b260a04 COMMIT THAT SHOULD BE SKIPPED NOT INCLUDED IN VERSION BUCKETS 2025-07-11 13:20:59 -07:00
Sam Crowder cad2a84e26 remove 0.2.86 2025-07-11 13:01:26 -07:00
233 changed files with 9559 additions and 11100 deletions
+11 -11
View File
@@ -1,29 +1,29 @@
name: "\U0001F41B Bug Report"
description: Report a bug in LangGraph. To report a security issue, please instead use the security option below. For questions, please use the LangChain Forum at forum.langchain.com.
description: Report a bug in LangGraph. To report a security issue, please instead use the security option below. For questions, please use the GitHub Discussions.
labels: [pending,bug]
body:
- type: markdown
attributes:
value: |
value: >
Thank you for taking the time to file a bug report.
Use this to report BUGS in LangGraph. For usage questions, feature requests and general design questions, please use the [LangChain Forum](https://forum.langchain.com/).
Use this to report BUGS in LangGraph. For usage questions, feature requests and general design questions, please use [GitHub Discussions](https://github.com/langchain-ai/langgraph/discussions).
Relevant links to check before filing a bug report to see if your issue has already been reported, fixed or
if there's another way to solve your problem:
* [LangChain Forum](https://forum.langchain.com/),
* [LangGraph Github Issues](https://github.com/langchain-ai/langgraph/issues),
* [LangGraph how-to guides](https://langchain-ai.github.io/langgraph/how-tos/).
* [LangChain documentation with the integrated search](https://python.langchain.com/docs/get_started/introduction),
* [GitHub search](https://github.com/langchain-ai/langgraph),
[LangGraph Github Discussions](https://github.com/langchain-ai/langgraph/discussions),
[LangGraph Github Issues](https://github.com/langchain-ai/langgraph/issues),
[LangGraph how-to guides](https://langchain-ai.github.io/langgraph/how-tos/).
[LangChain documentation with the integrated search](https://python.langchain.com/docs/get_started/introduction),
[GitHub search](https://github.com/langchain-ai/langgraph),
- type: checkboxes
id: checks
attributes:
label: Checked other resources
description: Before submitting this issue, please confirm that you have completed all the steps below by checking each option. These steps help ensure your issue is well-defined, relevant, and actionable.
options:
- label: This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
- label: This is a bug, not a usage question. For questions, please use GitHub Discussions.
required: true
- label: I added a clear and detailed title that summarizes the issue.
required: true
@@ -38,7 +38,7 @@ body:
attributes:
label: Example Code
description: |
Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. Replace this code with your own!
Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case.
placeholder: |
from langgraph.graph import StateGraph
@@ -78,7 +78,7 @@ body:
attributes:
label: System Info
description: |
Run on your machine: `python -m langchain_core.sys_info`
python -m langchain_core.sys_info
placeholder: |
python -m langchain_core.sys_info
validations:
+4 -2
View File
@@ -1,6 +1,8 @@
blank_issues_enabled: false
version: 2.1
contact_links:
- name: Feature Request
url: https://github.com/langchain-ai/langgraph/discussions/categories/ideas
about: Suggest a feature or an idea
- name: LangChain Forum
url: https://forum.langchain.com/
about: General community discussions, support, and feature requests
about: General community discussions and support
+8 -12
View File
@@ -1,29 +1,25 @@
name: 🔒 Privileged
description: You are a LangGraph maintainer, or was asked directly by a maintainer to create an issue here. If not, check the other options.
description: You are a LangChain maintainer, or was asked directly by a maintainer to create an issue here. If not, check the other options.
body:
- type: markdown
attributes:
value: |
Thanks for your interest in LangGraph! 🚀
If you are not a LangGraph maintainer or were not asked directly by a maintainer to create an issue, then please start the conversation on the [LangChain Forum](https://forum.langchain.com/) instead.
You are a LangGraph maintainer if you maintain any of the packages inside of the LangGraph repository
or are a regular contributor to LangGraph with previous merged merged pull requests.
Thanks for your interest in LangChain! 🚀
If you are not a LangChain maintainer or were not asked directly by a maintainer to create an issue, then please start the conversation in a [Question in GitHub Discussions](https://github.com/langchain-ai/langchain/discussions/categories/q-a) instead.
You are a LangChain maintainer if you maintain any of the packages inside of the LangChain repository
or are a regular contributor to LangChain with previous merged merged pull requests.
- type: checkboxes
id: privileged
attributes:
label: Privileged issue
description: Confirm that you are allowed to create an issue here.
options:
- label: I am a LangGraph maintainer, or was asked directly by a LangGraph maintainer to create an issue here.
- label: I am a LangChain maintainer, or was asked directly by a LangChain maintainer to create an issue here.
required: true
- type: textarea
id: content
attributes:
label: Issue Content
description: Add the content of the issue here.
- type: markdown
attributes:
value: |
Community members should **NOT** work on Privileged issues unless these issues have been explicitly marked with a "help-wanted" tag.
-31
View File
@@ -1,31 +0,0 @@
Thank you for contributing to LangGraph! Follow these steps to mark your pull request as ready for review. **If any of these steps are not completed, your PR will not be considered for review.**
- [ ] **PR title**: Follows the format: {TYPE}({SCOPE}): {DESCRIPTION}
- Examples:
- feat(core): add multi-tenant support
- fix(cli): resolve flag parsing error
- docs(openai): update API usage examples
- Allowed `{TYPE}` values:
- feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, release
- Allowed `{SCOPE}` values (optional):
- langgraph, docs, cli, checkpoint, checkpoint-postgres, checkpoint-sqlite, prebuilt, scheduler-kafka, sdk-py
- Once you've written the title, please delete this checklist item; do not include it in the PR.
- [ ] **PR message**: ***Delete this entire checklist*** and replace with
- **Description:** a description of the change. Include a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) if applicable.
- **Issue:** the issue # it fixes, if applicable
- **Dependencies:** any dependencies required for this change
- **Twitter handle:** if your PR gets announced, and you'd like a mention, we'll gladly shout you out!
- [ ] **Add tests and docs**: If you're adding a new integration, you must include:
1. A test for the integration, preferably unit tests that do not rely on network access,
2. An example notebook showing its use. It lives in `docs/docs/integrations` directory.
- [ ] **Lint and test**: Run `make format`, `make lint` and `make test` from the root of the package(s) you've modified. We will not consider a PR unless these three are passing in CI. See [contribution guidelines](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md) for more.
Additional guidelines:
- Make sure optional dependencies are imported within a function.
- Please do not add dependencies to `pyproject.toml` files (even optional ones) unless they are **required** for unit tests.
- Most PRs should not touch more than one package.
- Changes should be backwards compatible.
+1 -1
View File
@@ -3,7 +3,7 @@ name: CI
on:
push:
branches: [main, v1]
branches: [main]
pull_request:
permissions:
@@ -1,11 +0,0 @@
LangChain
LangGraph
LangSmith
thead
stdio
nd
jupyter
lets
lite
uis
deque
+3 -9
View File
@@ -34,16 +34,10 @@
id: extract_ignore_words
- name: Codespell
uses: codespell-project/actions-codespell@v2.1
uses: codespell-project/actions-codespell@v2
with:
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.css.map,*.js.map'
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.md'
ignore_words_list: ${{ steps.extract_ignore_words.outputs.ignore_words_list }}
# We do this to avoid spellchecking cell outputs
- name: Codespell Notebooks
run: make codespell
- name: Codespell LangGraph Library
run: |
# Change to root directory to check the main LangGraph library
cd ..
codespell --skip="*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.css.map,*.js.map,*.pyc,__pycache__/*" --ignore-words-list="${{ steps.extract_ignore_words.outputs.ignore_words_list }}" libs/langgraph/langgraph/
run: make codespell
+9 -7
View File
@@ -1,3 +1,4 @@
TESTING
# Contributing to LangGraph
Thank you for being interested in contributing to LangGraph!
@@ -9,7 +10,7 @@ Here are some things to keep in mind for all types of contributions:
- Follow the ["fork and pull request"](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project) workflow.
- Fill out the checked-in pull request template when opening pull requests. Note related issues and tag relevant maintainers.
- Ensure your PR passes formatting, linting, and testing checks before requesting a review.
- If you would like comments or feedback, please tag a maintainer.
- If you would like comments or feedback, please open an issue or discussion and tag a maintainer.
- Backwards compatibility is key. Your changes must not be breaking, except in case of critical bug and security fixes.
- Look for duplicate PRs or issues that have already been opened before opening a new one.
- Keep scope as isolated as possible. As a general rule, your changes should not affect more than one package at a time.
@@ -20,7 +21,7 @@ For bug fixes, please open up an issue before proposing a fix to ensure the prop
### New features
For new features, please start a new [discussion](https://forum.langchain.com/), where the maintainers will help with scoping out the necessary changes.
For new features, please start a new [discussion](https://github.com/langchain-ai/langgraph/discussions), where the maintainers will help with scoping out the necessary changes.
## Contribute Documentation
@@ -111,6 +112,7 @@ in a more abstract way than how-to guides or tutorials, and should be geared tow
gaining a deeper understanding of the framework. Try to avoid excessively large code examples. The goal here is to
impart perspective to the user rather than to finish a practical project. These guides should cover **why** things work the way they do.
To quote the Diataxis website:
> The perspective of explanation is higher and wider than that of the other types. It does not take the users eye-level view, as in a how-to guide, or a close-up view of the machinery, like reference material. Its scope in each case is a topic - “an area of knowledge”, that somehow has to be bounded in a reasonable, meaningful way.
@@ -186,9 +188,9 @@ Be concise, including in code samples.
## Setup
LangGraph documentation consists of two components:
LangChain documentation consists of two components:
1. Main Documentation: Hosted at [https://langchain-ai.github.io/langgraph/](https://langchain-ai.github.io/langgraph/),
1. Main Documentation: Hosted at [https://langchain-ai.github.io](https://langchain-ai.github.io/langgraph/),
this comprehensive resource serves as the primary user-facing documentation.
It covers a wide array of topics, including tutorials, use cases, integrations,
and more, offering extensive guidance on building with LangGraph.
@@ -249,17 +251,17 @@ make serve-docs
#### Linting
To spell check the docs, run the following from the `docs` directory:
The documentation is linted from the **monorepo root**. To lint it, run the following from there:
```bash
codespell --skip="*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.css.map,*.js.map" --ignore-words-list="infor,thead,stdio,nd,jupyter,lets,lite,uis,deque" .
make spellcheck
```
### In-code Documentation
The in-code documentation is autogenerated from docstrings.
For the API reference to be useful, the codebase must be well-documented. This means that all functions, classes, and methods should have a docstring that explains what they do, what the arguments are, and what the return value is. This is a good practice in general, but it is especially important for LangGraph because the API reference is the primary resource for developers to understand how to use the codebase.
For the API reference to be useful, the codebase must be well-documented. This means that all functions, classes, and methods should have a docstring that explains what they do, what the arguments are, and what the return value is. This is a good practice in general, but it is especially important for LangChain because the API reference is the primary resource for developers to understand how to use the codebase.
We generally follow the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for docstrings.
+2 -3
View File
@@ -73,12 +73,11 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
- [Guides](https://langchain-ai.github.io/langgraph/how-tos/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
- [Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
- [Examples](https://langchain-ai.github.io/langgraph/examples/): Guided examples on getting started with LangGraph.
- [LangChain Forum](https://forum.langchain.com/): Connect with the community and share all of your technical questions, ideas, and feedback.
- [Examples](https://langchain-ai.github.io/langgraph/tutorials/overview/): Guided examples on getting started with LangGraph.
- [LangChain Academy](https://academy.langchain.com/courses/intro-to-langgraph): Learn the basics of LangGraph in our free, structured course.
- [Templates](https://langchain-ai.github.io/langgraph/concepts/template_applications/): Pre-built reference apps for common agentic workflows (e.g. ReAct agent, memory, retrieval etc.) that can be cloned and adapted.
- [Case studies](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship AI applications at scale.
## Acknowledgements
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
+162
View File
@@ -0,0 +1,162 @@
"""
Copy page functionality hooks for MkDocs.
This module provides hooks to inject original markdown content into HTML pages
for the copy page functionality, allowing users to copy clean markdown content
optimized for LLMs.
"""
import json
import re
from pathlib import Path
from typing import Optional
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.structure.pages import Page
def _process_includes(content: str, docs_dir: Path) -> str:
"""Process MkDocs includes like {!../README.md!}."""
include_pattern = r'\{!([^!]+)!\}'
def replace_include(match):
include_path = match.group(1)
# Resolve relative path
if include_path.startswith('../'):
# Go up from docs dir
include_file = docs_dir.parent / include_path[3:]
else:
include_file = docs_dir / include_path
try:
with open(include_file, 'r', encoding='utf-8') as f:
included_content = f.read()
# Remove frontmatter from included content to avoid duplication
included_content = re.sub(r'^---\n.*?\n---\n', '', included_content, flags=re.DOTALL)
return included_content
except:
return f"[Content from {include_path}]"
return re.sub(include_pattern, replace_include, content)
def _clean_markdown(content: str) -> str:
"""Minimal cleanup of markdown content - preserve original as much as possible."""
# Remove frontmatter
content = re.sub(r'^---\n.*?\n---\n', '', content, flags=re.DOTALL)
# Remove script tags (security)
content = re.sub(r'<script[^>]*>.*?</script\s*>', '', content, flags=re.DOTALL | re.IGNORECASE)
# Remove style tags (security)
content = re.sub(r'<style[^>]*>.*?</style\s*>', '', content, flags=re.DOTALL | re.IGNORECASE)
# Remove HTML comments
content = re.sub(r'<!--.*?-->', '', content, flags=re.DOTALL)
# Just strip and return - preserve original structure
return content.strip()
def inject_markdown_content(html: str, page: Page, config: MkDocsConfig) -> str:
"""
Inject the original markdown content into the HTML for copy page functionality.
Args:
html: The HTML content to inject into
page: The MkDocs page object
config: The MkDocs configuration
Returns:
Modified HTML with markdown content injected as JSON
"""
if not hasattr(page, 'file') or not page.file:
return html
# Get the original markdown file path
docs_dir = Path(config.get('docs_dir', 'docs'))
src_path = page.file.src_path
# Handle different file types
if src_path.endswith('.ipynb'):
# For notebook files, we might want to use the converted markdown
# For now, just return the HTML as-is
return html
markdown_file = docs_dir / src_path
if not markdown_file.exists():
return html
try:
# Read the original markdown content
with open(markdown_file, 'r', encoding='utf-8') as f:
markdown_content = f.read()
# Special handling for index page - use relative path to the actual README.md
if src_path == 'index.md':
# Relative path to the repository README.md file (go up two levels from docs/docs)
readme_path = docs_dir.parent.parent / 'README.md'
try:
with open(readme_path, 'r', encoding='utf-8') as f:
readme_content = f.read()
# Remove frontmatter if present
processed_markdown = re.sub(r'^---\n.*?\n---\n', '', readme_content, flags=re.DOTALL)
processed_markdown = processed_markdown.strip()
except Exception as e:
# If we can't read the README, fallback to original behavior
processed_markdown = _process_includes(markdown_content, docs_dir)
processed_markdown = re.sub(r'^---\n.*?\n---\n', '', processed_markdown, flags=re.DOTALL)
processed_markdown = processed_markdown.strip()
else:
# Process any includes in the markdown to get the full content
processed_markdown = _process_includes(markdown_content, docs_dir)
# Clean up the processed markdown normally for other pages
processed_markdown = _clean_markdown(processed_markdown)
# Create the JSON data
markdown_data = {
'markdown': processed_markdown,
'title': page.title or 'Page Content',
'url': page.url or ''
}
# Properly escape the JSON for HTML
json_content = json.dumps(markdown_data, ensure_ascii=False)
json_content = json_content.replace('</', '\\u003c/')
json_content = json_content.replace('<script', '\\u003cscript')
json_content = json_content.replace('</script', '\\u003c/script')
script_content = f'<script id="page-markdown-content" type="application/json">{json_content}</script>'
# Insert before </head> if it exists, otherwise before </body>
if '</head>' in html:
html = html.replace('</head>', f'{script_content}</head>')
elif '</body>' in html:
html = html.replace('</body>', f'{script_content}</body>')
except Exception as e:
# If anything goes wrong, just return the original HTML
# Could log the error here if needed
pass
return html
def on_post_page(output: str, page: Page, config: MkDocsConfig) -> str:
"""
MkDocs hook to inject markdown content into HTML pages.
This hook is called after each page is rendered and injects the original
markdown content as JSON for the copy page functionality.
Args:
output: The HTML output of the page
page: The MkDocs page object
config: The MkDocs configuration
Returns:
Modified HTML with markdown content injected
"""
return inject_markdown_content(output, page, config)
+14 -51
View File
@@ -3,7 +3,6 @@
Lifecycle events: https://www.mkdocs.org/dev-guide/plugins/#events
"""
import json
import logging
import os
import posixpath
@@ -16,8 +15,8 @@ from mkdocs.structure.files import Files, File
from mkdocs.structure.pages import Page
from _scripts.generate_api_reference_links import update_markdown_with_imports
from _scripts.link_map import JS_LINK_MAP
from _scripts.notebook_convert import convert_notebook
from _scripts.link_map import JS_LINK_MAP
logger = logging.getLogger(__name__)
logging.basicConfig()
@@ -101,6 +100,10 @@ REDIRECT_MAP = {
"how-tos/create-react-agent-memory.ipynb": "agents/memory.md",
"how-tos/create-react-agent-system-prompt.ipynb": "agents/context.md#prompts",
"how-tos/create-react-agent-structured-output.ipynb": "agents/agents.md#structured-output",
# Time-travel
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "how-tos/human_in_the_loop/time-travel.md",
# breakpoints
"how-tos/human_in_the_loop/dynamic_breakpoints.ipynb": "how-tos/human_in_the_loop/breakpoints.md",
# misc
"prebuilt.md": "agents/prebuilt.md",
"reference/prebuilt.md": "reference/agents.md",
@@ -122,11 +125,6 @@ REDIRECT_MAP = {
"how-tos/review-tool-calls-functional.ipynb": "how-tos/use-functional-api.md",
"how-tos/create-react-agent-hitl.ipynb": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
"agents/human-in-the-loop.md": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
"how-tos/human_in_the_loop/dynamic_breakpoints.ipynb": "how-tos/human_in_the_loop/breakpoints.md",
"concepts/breakpoints.md": "concepts/human_in_the_loop.md",
"how-tos/human_in_the_loop/breakpoints.md": "how-tos/human_in_the_loop/add-human-in-the-loop.md",
"cloud/how-tos/human_in_the_loop_breakpoint.md": "cloud/how-tos/add-human-in-the-loop.md",
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "how-tos/human_in_the_loop/time-travel.md",
}
@@ -358,16 +356,12 @@ def _on_page_markdown_with_config(
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
finalized_markdown = (
_on_page_markdown_with_config(
markdown,
page,
add_api_references=True,
**kwargs,
)
return _on_page_markdown_with_config(
markdown,
page,
add_api_references=True,
**kwargs,
)
page.meta["original_markdown"] = finalized_markdown
return finalized_markdown
# redirects
@@ -437,51 +431,20 @@ height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
else:
return html # fallback if no <body> found
def _inject_markdown_into_html(html: str, page: Page) -> str:
"""Inject the original markdown content into the HTML page as JSON."""
original_markdown = page.meta.get("original_markdown", "")
if not original_markdown:
return html
markdown_data = {
"markdown": original_markdown,
"title": page.title or "Page Content",
"url": page.url or "",
}
# Properly escape the JSON for HTML
json_content = json.dumps(markdown_data, ensure_ascii=False)
json_content = (
json_content.replace("</", "\\u003c/")
.replace("<script", "\\u003cscript")
.replace("</script", "\\u003c/script")
)
script_content = (
f'<script id="page-markdown-content" '
f'type="application/json">{json_content}</script>'
)
# Insert before </head> if it exists, otherwise before </body>
if "</head>" not in html:
raise ValueError(
"HTML does not contain </head> tag. Cannot inject markdown content."
)
return html.replace("</head>", f"{script_content}</head>")
def on_post_page(html: str, page: Page, config: MkDocsConfig) -> str:
def on_post_page(output: str, page: Page, config: MkDocsConfig) -> str:
"""Inject Google Tag Manager noscript tag immediately after <body>.
Args:
html: The HTML output of the page.
output: The HTML output of the page.
page: The page instance.
config: The MkDocs configuration object.
Returns:
modified HTML output with GTM code injected.
"""
html = _inject_markdown_into_html(html, page)
return _inject_gtm(html)
return _inject_gtm(output)
# Create HTML files for redirects after site dir has been built
def on_post_build(config):
+22 -32
View File
@@ -12,64 +12,56 @@ LangGraph provides **three** primary ways to supply context:
| Type | Description | Mutable? | Lifetime |
|------------------------------------------------------------------------------|-----------------------------------------------|----------|-------------------------|
| [**Runtime Context**](#runtime-context) | data passed at the start of a run | ❌ | per run |
| [**Config**](#config-static-context) | data passed at the start of a run | ❌ | per run |
| [**Short-term memory (State)**](#short-term-memory-mutable-context) | dynamic data that can change during execution | ✅ | per run or conversation |
| [**Long-term memory (Store)**](#long-term-memory-cross-conversation-context) | data that can be shared between conversations | ✅ | across conversations |
### Runtime Context
## Provide runtime context
!!! note "`config['configurable']` -> `runtime.context`"
### Config (static context)
In LangGraph < v1.0, static runtime context was passed via the `config['configurable']` key, paired with a `config_schema` argument
to `StateGraph` or `Pregel`. This is now deprecated and will be removed in v2.0.
Config is for immutable data like user metadata or API keys. Use
when you have values that don't change mid-run.
As of LangGraph v1.0, the Runtime object is recommended to access static context and runtime-specific information like the store and stream writer.
Runtime context is for immutable data like user metadata or API keys. Use this when you have values that don't change mid-run.
Specify static context via the `context` argument to `invoke` / `stream`, which is reserved for this purpose:
Specify configuration using a key called **"configurable"** which is reserved
for this purpose:
```python
@dataclass
class ContextSchema:
user_name: str
graph.invoke( # (1)!
{"messages": [{"role": "user", "content": "hi!"}]}, # (2)!
# highlight-next-line
context={"user_name": "John Smith"} # (3)!
config={"configurable": {"user_id": "user_123"}} # (3)!
)
```
1. This is the invocation of the agent or graph. The `invoke` method runs the underlying graph with the provided input.
2. This example uses messages as an input, which is common, but your application may use different input structures.
3. This is where you pass the runtime data. The `context` parameter allows you to provide additional dependencies that the agent can use during its execution.
3. This is where you pass the configuration data. The `config` parameter allows you to provide additional context that the agent can use during its execution.
=== "Agent prompt"
```python
from langchain_core.messages import AnyMessage
from langgraph.runtime import get_runtime
from langchain_core.runnables import RunnableConfig
from langgraph.prebuilt.chat_agent_executor import AgentState
from langgraph.prebuilt import create_react_agent
# highlight-next-line
def prompt(state: AgentState) -> list[AnyMessage]:
runtime = get_runtime(ContextSchema)
system_msg = f"You are a helpful assistant. Address the user as {runtime.context.user_name}."
def prompt(state: AgentState, config: RunnableConfig) -> list[AnyMessage]:
user_name = config["configurable"].get("user_name")
system_msg = f"You are a helpful assistant. Address the user as {user_name}."
return [{"role": "system", "content": system_msg}] + state["messages"]
agent = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[get_weather],
prompt=prompt,
context_schema=ContextSchema
prompt=prompt
)
agent.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
# highlight-next-line
context={"user_name": "John Smith"}
config={"configurable": {"user_name": "John Smith"}}
)
```
@@ -78,11 +70,11 @@ graph.invoke( # (1)!
=== "Workflow node"
```python
from langgraph.runtime import Runtime
from langchain_core.runnables import RunnableConfig
# highlight-next-line
def node(state: State, config: Runtime[ContextSchema]):
user_name = runtime.context.user_name
def node(state: State, config: RunnableConfig):
user_name = config["configurable"].get("user_name")
...
```
@@ -91,16 +83,14 @@ graph.invoke( # (1)!
=== "In a tool"
```python
from langgraph.runtime import get_runtime
from langchain_core.runnables import RunnableConfig
@tool
# highlight-next-line
def get_user_email() -> str:
def get_user_info(config: RunnableConfig) -> str:
"""Retrieve user information based on user ID."""
# simulate fetching user info from a database
runtime = get_runtime(ContextSchema)
email = get_user_email_from_db(runtime.context.user_name)
return email
user_id = config["configurable"].get("user_id")
return "User is John Smith" if user_id == "user_123" else "Unknown user"
```
See the [tool calling guide](../how-tos/tool-calling.md#configuration) for details.
+14 -41
View File
@@ -55,16 +55,14 @@ The `langchain-mcp-adapters` package enables agents to use tools defined across
=== "In a workflow"
```python title="Workflow using MCP tools with ToolNode"
```python
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.prebuilt import ToolNode, tools_condition
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
model = init_chat_model("openai:gpt-4.1")
# Initialize the model
model = init_chat_model("anthropic:claude-3-5-sonnet-latest")
# Set up MCP client
client = MultiServerMCPClient(
{
"math": {
@@ -82,47 +80,22 @@ The `langchain-mcp-adapters` package enables agents to use tools defined across
)
tools = await client.get_tools()
# Bind tools to model
model_with_tools = model.bind_tools(tools)
def call_model(state: MessagesState):
response = model.bind_tools(tools).invoke(state["messages"])
return {"messages": response}
# Create ToolNode
tool_node = ToolNode(tools)
def should_continue(state: MessagesState):
messages = state["messages"]
last_message = messages[-1]
if last_message.tool_calls:
return "tools"
return END
# Define call_model function
async def call_model(state: MessagesState):
messages = state["messages"]
response = await model_with_tools.ainvoke(messages)
return {"messages": [response]}
# Build the graph
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_node("tools", tool_node)
builder.add_node(call_model)
builder.add_node(ToolNode(tools))
builder.add_edge(START, "call_model")
builder.add_conditional_edges(
"call_model",
should_continue,
tools_condition,
)
builder.add_edge("tools", "call_model")
# Compile the graph
graph = builder.compile()
# Test the graph
math_response = await graph.ainvoke(
{"messages": [{"role": "user", "content": "what's (3 + 5) x 12?"}]}
)
weather_response = await graph.ainvoke(
{"messages": [{"role": "user", "content": "what is the weather in nyc?"}]}
)
math_response = await graph.ainvoke({"messages": "what's (3 + 5) x 12?"})
weather_response = await graph.ainvoke({"messages": "what is the weather in nyc?"})
```
@@ -175,4 +148,4 @@ if __name__ == "__main__":
- [MCP documentation](https://modelcontextprotocol.io/introduction)
- [MCP Transport documentation](https://modelcontextprotocol.io/docs/concepts/transports)
- [langchain_mcp_adapters](https://github.com/langchain-ai/langchain-mcp-adapters)
- [langchain_mcp_adapters](https://github.com/langchain-ai/langchain-mcp-adapters)
+1 -1
View File
@@ -7,7 +7,7 @@ LangGraph provides built-in support for [LLMs (language models)](https://python.
Use [`init_chat_model`](https://python.langchain.com/docs/how_to/chat_models_universal_init/) to initialize models:
{% include-markdown "../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
### Instantiate a model directly
-119
View File
@@ -1,119 +0,0 @@
# Egress for Subscription Metrics and Operational Metadata
> **Important: Self Hosted Only**
> This section only applies to customers who are not running in offline mode and assumes you are using a self-hosted LangGraph Platform instance.
> This does not apply to SaaS or Hybrid deployments.
Self-Hosted LangGraph Platform instances store all information locally and will never send sensitive information outside of your network. We currently only track platform usage for billing purposes according to the entitlements in your order. In order to better remotely support our customers, we do require egress to `https://beacon.langchain.com`.
In the future, we will be introducing support diagnostics to help us ensure that the LangGraph Platform is running at an optimal level within your environment.
> **Warning**
> **This will require egress to `https://beacon.langchain.com` from your network.**
> **If using an API key, you will also need to allow egress to `https://api.smith.langchain.com` or `https://eu.api.smith.langchain.com` for API key verification.**
Generally, data that we send to Beacon can be categorized as follows:
- **Subscription Metrics**
- Subscription metrics are used to determine level of access and utilization of LangSmith. This includes, but are not limited to:
- Nodes Executed
- Runs Executed
- License Key Verification
- **Operational Metadata**
- This metadata will contain and collect the above subscription metrics to assist with remote support, allowing the LangChain team to diagnose and troubleshoot performance issues more effectively and proactively.
## Example Payloads
In an effort to maximize transparency, we provide sample payloads here:
### License Verification (If using an Enterprise License)
**Endpoint:**
`POST beacon.langchain.com/v1/beacon/verify`
**Request:**
```json
{
"license": "<YOUR_LICENSE_KEY>"
}
```
**Response:**
```json
{
"token": "Valid JWT" // Short-lived JWT token to avoid repeated license checks
}
```
### Api Key Verification (If using a LangSmith API Key)
**Endpoint:**
`POST api.smith.langchain.com/auth`
**Request:**
```json
"Headers": {
X-Api-Key: <YOUR_API_KEY>
}
```
**Response:**
```json
{
"org_config": {
"org_id": "3a1c2b6f-4430-4b92-8a5b-79b8b567bbc1",
... // Additional organization details
}
}
```
### Usage Reporting
**Endpoint:**
`POST beacon.langchain.com/v1/metadata/submit`
**Request:**
```json
{
"license": "<YOUR_LICENSE_KEY>",
"from_timestamp": "2025-01-06T09:00:00Z",
"to_timestamp": "2025-01-06T10:00:00Z",
"tags": {
"langgraph.python.version": "0.1.0",
"langgraph_api.version": "0.2.0",
"langgraph.platform.revision": "abc123",
"langgraph.platform.variant": "standard",
"langgraph.platform.host": "host-1",
"langgraph.platform.tenant_id": "3a1c2b6f-4430-4b92-8a5b-79b8b567bbc1",
"langgraph.platform.project_id": "c5b5f53a-4716-4326-8967-d4f7f7799735",
"langgraph.platform.plan": "enterprise",
"user_app.uses_indexing": "true",
"user_app.uses_custom_app": "false",
"user_app.uses_custom_auth": "true",
"user_app.uses_thread_ttl": "true",
"user_app.uses_store_ttl": "false"
},
"measures": {
"langgraph.platform.runs": 150,
"langgraph.platform.nodes": 450
},
"logs": []
}
```
**Response:**
```json
"204 No Content"
```
## Our Commitment
LangChain will not store any sensitive information in the Subscription Metrics or Operational Metadata. Any data collected will not be shared with a third party. If you have any concerns about the data being sent, please reach out to your account team.
@@ -23,8 +23,6 @@ Before deploying, review the [conceptual guide for the Self-Hosted Control Plane
kubectl get storageclass
1. Egress to `https://beacon.langchain.com` from your network. This is required for license verification and usage reporting if not running in air-gapped mode. See the [Egress documentation](../../cloud/deployment/egress.md) for more details.
## Setup
1. As part of configuring your Self-Hosted LangSmith instance, you enable the `langgraphPlatform` option. This will provision a few key resources.
@@ -35,6 +35,7 @@ Before deploying, review the [conceptual guide for the Self-Hosted Data Plane](.
1. Configure your `langgraph-dataplane-values.yaml` file.
config:
langgraphPlatformLicenseKey: "" # Your LangGraph Platform license key
langsmithApiKey: "" # API Key of your Workspace
langsmithWorkspaceId: "" # Workspace ID
hostBackendUrl: "https://api.host.langchain.com" # Only override this if on EU
+3 -3
View File
@@ -108,11 +108,11 @@ from langgraph.graph import StateGraph, END, START
from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes
from my_agent.utils.state import AgentState # import state
# Define the runtime context
class GraphContext(TypedDict):
# Define the config
class GraphConfig(TypedDict):
model_name: Literal["anthropic", "openai"]
workflow = StateGraph(AgentState, context_schema=GraphContext)
workflow = StateGraph(AgentState, config_schema=GraphConfig)
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
workflow.add_edge(START, "agent")
@@ -121,11 +121,11 @@ from langgraph.graph import StateGraph, END, START
from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes
from my_agent.utils.state import AgentState # import state
# Define the runtime context
class GraphContext(TypedDict):
# Define the config
class GraphConfig(TypedDict):
model_name: Literal["anthropic", "openai"]
workflow = StateGraph(AgentState, context_schema=GraphContext)
workflow = StateGraph(AgentState, config_schema=GraphConfig)
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
workflow.add_edge(START, "agent")
@@ -24,7 +24,6 @@ Before deploying, review the [conceptual guide for the Standalone Container](../
1. `LANGSMITH_API_KEY`: (if using [Lite](../../concepts/langgraph_server.md#server-versions)) LangSmith API key. This will be used to authenticate ONCE at server start up.
1. `LANGGRAPH_CLOUD_LICENSE_KEY`: (if using [Enterprise](../../concepts/langgraph_data_plane.md#licensing)) LangGraph Platform license key. This will be used to authenticate ONCE at server start up.
1. `LANGSMITH_ENDPOINT`: To send traces to a [self-hosted LangSmith](https://docs.smith.langchain.com/self_hosting) instance, set `LANGSMITH_ENDPOINT` to the hostname of the self-hosted LangSmith instance.
1. Egress to `https://beacon.langchain.com` from your network. This is required for license verification and usage reporting if not running in air-gapped mode. See the [Egress documentation](../../cloud/deployment/egress.md) for more details.
## Kubernetes (Helm)
@@ -2,7 +2,7 @@
To review, edit, and approve tool calls in an agent or workflow, use LangGraph's [human-in-the-loop](../../concepts/human_in_the_loop.md) features.
## Dynamic interrupts
## LangGraph API invoke & resume
=== "Python"
@@ -30,7 +30,9 @@ To review, edit, and approve tool calls in an agent or workflow, use LangGraph's
# > [
# > {
# > 'value': {'text_to_revise': 'original text'},
# > 'id': '...',
# > 'resumable': True,
# > 'ns': ['human_node:fc722478-2f21-0578-c572-d9fc4dd07c3b'],
# > 'when': 'during'
# > }
# > ]
@@ -201,7 +203,9 @@ To review, edit, and approve tool calls in an agent or workflow, use LangGraph's
# > [
# > {
# > 'value': {'text_to_revise': 'original text'},
# > 'id': '...',
# > 'resumable': True,
# > 'ns': ['human_node:fc722478-2f21-0578-c572-d9fc4dd07c3b'],
# > 'when': 'during'
# > }
# > ]
@@ -301,185 +305,6 @@ To review, edit, and approve tool calls in an agent or workflow, use LangGraph's
}"
```
## Static interrupts
Static interrupts (also known as static breakpoints) are triggered either before or after a node executes.
!!! warning
Static interrupts are **not** recommended for human-in-the-loop workflows. They are best used for debugging and testing.
You can set static interrupts by specifying `interrupt_before` and `interrupt_after` at compile time:
```python
# highlight-next-line
graph = graph_builder.compile( # (1)!
# highlight-next-line
interrupt_before=["node_a"], # (2)!
# highlight-next-line
interrupt_after=["node_b", "node_c"], # (3)!
)
```
1. The breakpoints are set during `compile` time.
2. `interrupt_before` specifies the nodes where execution should pause before the node is executed.
3. `interrupt_after` specifies the nodes where execution should pause after the node is executed.
Alternatively, you can set static interrupts at run time:
=== "Python"
```python
# highlight-next-line
await client.runs.wait( # (1)!
thread_id,
assistant_id,
inputs=inputs,
# highlight-next-line
interrupt_before=["node_a"], # (2)!
# highlight-next-line
interrupt_after=["node_b", "node_c"] # (3)!
)
```
1. `client.runs.wait` is called with the `interrupt_before` and `interrupt_after` parameters. This is a run-time configuration and can be changed for every invocation.
2. `interrupt_before` specifies the nodes where execution should pause before the node is executed.
3. `interrupt_after` specifies the nodes where execution should pause after the node is executed.
=== "JavaScript"
```js
// highlight-next-line
await client.runs.wait( // (1)!
threadID,
assistantID,
{
input: input,
// highlight-next-line
interruptBefore: ["node_a"], // (2)!
// highlight-next-line
interruptAfter: ["node_b", "node_c"] // (3)!
}
)
```
1. `client.runs.wait` is called with the `interruptBefore` and `interruptAfter` parameters. This is a run-time configuration and can be changed for every invocation.
2. `interruptBefore` specifies the nodes where execution should pause before the node is executed.
3. `interruptAfter` specifies the nodes where execution should pause after the node is executed.
=== "cURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/wait \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"interrupt_before\": [\"node_a\"],
\"interrupt_after\": [\"node_b\", \"node_c\"],
\"input\": <INPUT>
}"
```
The following example shows how to add static interrupts:
=== "Python"
```python
from langgraph_sdk import get_client
client = get_client(url=<DEPLOYMENT_URL>)
# Using the graph deployed with the name "agent"
assistant_id = "agent"
# create a thread
thread = await client.threads.create()
thread_id = thread["thread_id"]
# Run the graph until the breakpoint
result = await client.runs.wait(
thread_id,
assistant_id,
input=inputs # (1)!
)
# Resume the graph
await client.runs.wait(
thread_id,
assistant_id,
input=None # (2)!
)
```
1. The graph is run until the first breakpoint is hit.
2. The graph is resumed by passing in `None` for the input. This will run the graph until the next breakpoint is hit.
=== "JavaScript"
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
// Using the graph deployed with the name "agent"
const assistantID = "agent";
// create a thread
const thread = await client.threads.create();
const threadID = thread["thread_id"];
// Run the graph until the breakpoint
const result = await client.runs.wait(
threadID,
assistantID,
{ input: input } // (1)!
);
// Resume the graph
await client.runs.wait(
threadID,
assistantID,
{ input: null } // (2)!
);
```
1. The graph is run until the first breakpoint is hit.
2. The graph is resumed by passing in `null` for the input. This will run the graph until the next breakpoint is hit.
=== "cURL"
Create a thread:
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json' \
--data '{}'
```
Run the graph until the breakpoint:
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/wait \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": <INPUT>
}"
```
Resume the graph:
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/wait \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\"
}"
```
## Learn more
- [Human-in-the-loop conceptual guide](../../concepts/human_in_the_loop.md): learn more about LangGraph human-in-the-loop features.
@@ -2,20 +2,21 @@
In this guide we will show how to create, configure, and manage an [assistant](../../concepts/assistants.md).
First, as a brief refresher on the concept of runtime context, consider the following simple `call_model` node and context schema. Observe that this node tries to read and use the `model_provider` as defined by the `Runtime` object's `context` property.
First, as a brief refresher on the concept of configurations, consider the following simple `call_model` node and configuration schema. Observe that this node tries to read and use the `model_name` as defined by the `config` object's `configurable`.
=== "Python"
```python
@dataclass
class ContextSchema:
llm_provider: str = "anthropic"
builder = StateGraph(AgentState, context_schema=ContextSchema)
class ConfigSchema(TypedDict):
model_name: str
def call_model(state, runtime: Runtime[ContextSchema]):
builder = StateGraph(AgentState, config_schema=ConfigSchema)
def call_model(state, config):
messages = state["messages"]
model = _get_model(runtime.context.llm_provider)
model_name = config.get('configurable', {}).get("model_name", "anthropic")
model = _get_model(model_name)
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
@@ -43,7 +44,7 @@ First, as a brief refresher on the concept of runtime context, consider the foll
}
```
For more information on runtime context, [see here](../../concepts/low_level.md#runtime-context).
For more information on configurations, [see here](../../concepts/low_level.md#configuration).
## Create an assistant
+11 -27
View File
@@ -30,33 +30,17 @@ export default {
Next, define your UI components in your `langgraph.json` configuration:
=== "Python agent"
```json title="langgraph.json"
{
"node_version": "20",
"graphs": {
"agent": "./src/agent.py:graph"
},
"ui": {
"agent": "./src/agent/ui.tsx"
}
}
```
=== "JS agent"
```json title="langgraph.json"
{
"node_version": "20",
"graphs": {
"agent": "./src/agent/index.ts:graph"
},
"ui": {
"agent": "./src/agent/ui.tsx"
}
}
```
```json
{
"node_version": "20",
"graphs": {
"agent": "./src/agent/index.ts:graph"
},
"ui": {
"agent": "./src/agent/ui.tsx"
}
}
```
The `ui` section points to the UI components that will be used by graphs. By default, we recommend using the same key as the graph name, but you can split out the components however you like, see [Customise the namespace of UI components](#customise-the-namespace-of-ui-components) for more details.
@@ -0,0 +1,185 @@
# Set breakpoints using Server API
[Breakpoints](../../concepts/breakpoints.md) pause graph execution at defined points and let you step through each stage. They use LangGraph's [**persistence layer**](../../concepts/persistence.md), which saves the graph state after each step.
With breakpoints, you can inspect the graph's state and node inputs at any point. Execution pauses indefinitely until you resume, as the checkpointer preserves the state.
!!! tip
For conceptual information on breakpoints, see [Breakpoints](../../concepts/breakpoints.md).
## Set static breakpoints
Static breakpoints are triggered either before or after a node executes. You can set static breakpoints by specifying `interrupt_before` and `interrupt_after` at compile time or run time.
=== "Compile time"
```python
# highlight-next-line
graph = graph_builder.compile( # (1)!
# highlight-next-line
interrupt_before=["node_a"], # (2)!
# highlight-next-line
interrupt_after=["node_b", "node_c"], # (3)!
)
```
1. The breakpoints are set during `compile` time.
2. `interrupt_before` specifies the nodes where execution should pause before the node is executed.
3. `interrupt_after` specifies the nodes where execution should pause after the node is executed.
=== "Run time"
=== "Python"
```python
# highlight-next-line
await client.runs.wait( # (1)!
thread_id,
assistant_id,
inputs=inputs,
# highlight-next-line
interrupt_before=["node_a"], # (2)!
# highlight-next-line
interrupt_after=["node_b", "node_c"] # (3)!
)
```
1. `client.runs.wait` is called with the `interrupt_before` and `interrupt_after` parameters. This is a run-time configuration and can be changed for every invocation.
2. `interrupt_before` specifies the nodes where execution should pause before the node is executed.
3. `interrupt_after` specifies the nodes where execution should pause after the node is executed.
=== "JavaScript"
```js
// highlight-next-line
await client.runs.wait( // (1)!
threadID,
assistantID,
{
input: input,
// highlight-next-line
interruptBefore: ["node_a"], // (2)!
// highlight-next-line
interruptAfter: ["node_b", "node_c"] // (3)!
}
)
```
1. `client.runs.wait` is called with the `interruptBefore` and `interruptAfter` parameters. This is a run-time configuration and can be changed for every invocation.
2. `interruptBefore` specifies the nodes where execution should pause before the node is executed.
3. `interruptAfter` specifies the nodes where execution should pause after the node is executed.
=== "cURL"
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/wait \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"interrupt_before\": [\"node_a\"],
\"interrupt_after\": [\"node_b\", \"node_c\"],
\"input\": <INPUT>
}"
```
## Example
This example shows how to add **static** breakpoints. See [Use breakpoints](../../how-tos/human_in_the_loop/breakpoints.md) for more options on adding breakpoints.
=== "Python"
```python
from langgraph_sdk import get_client
client = get_client(url=<DEPLOYMENT_URL>)
# Using the graph deployed with the name "agent"
assistant_id = "agent"
# create a thread
thread = await client.threads.create()
thread_id = thread["thread_id"]
# Run the graph until the breakpoint
result = await client.runs.wait(
thread_id,
assistant_id,
input=inputs # (1)!
)
# Resume the graph
await client.runs.wait(
thread_id,
assistant_id,
input=None # (2)!
)
```
1. The graph is run until the first breakpoint is hit.
2. The graph is resumed by passing in `None` for the input. This will run the graph until the next breakpoint is hit.
=== "JavaScript"
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
// Using the graph deployed with the name "agent"
const assistantID = "agent";
// create a thread
const thread = await client.threads.create();
const threadID = thread["thread_id"];
// Run the graph until the breakpoint
const result = await client.runs.wait(
threadID,
assistantID,
{ input: input } // (1)!
);
// Resume the graph
await client.runs.wait(
threadID,
assistantID,
{ input: null } // (2)!
);
```
1. The graph is run until the first breakpoint is hit.
2. The graph is resumed by passing in `null` for the input. This will run the graph until the next breakpoint is hit.
=== "cURL"
Create a thread:
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json' \
--data '{}'
```
Run the graph until the breakpoint:
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/wait \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": <INPUT>
}"
```
Resume the graph:
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/wait \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\"
}"
```
+1 -1
View File
@@ -29,7 +29,7 @@ Click the dropdown next to "Submit" and click the toggle to enable/disable strea
To run your graph with breakpoints, click the "Interrupt" button. Select a node and whether to pause before and/or after that node has executed. Click "Continue" in the thread log to resume execution.
For more information on breakpoints see [here](../../concepts/human_in_the_loop.md).
For more information on breakpoints see [here](../../concepts/breakpoints.md).
### Submit run
-16
View File
@@ -140,22 +140,6 @@ https://my-server.app/my-webhook-endpoint?token=YOUR_SECRET_TOKEN
Your server should extract and validate this token before processing requests.
## Disable webhooks
As of `langgraph-api>=0.2.78`, developers can disable webhooks in the `langgraph.json` file:
```json
{
"http": {
"disable_webhooks": true
}
}
```
This feature is primarily intended for self-hosted deployments, where platform administrators or developers may prefer to disable webhooks to simplify their security posture—especially if they are not configuring firewall rules or other network controls. Disabling webhooks helps prevent untrusted payloads from being sent to internal endpoints.
For full configuration details, refer to the [configuration file reference](https://langchain-ai.github.io/langgraph/cloud/reference/cli/?h=disable_webhooks#configuration-file).
## Test webhooks
You can test your webhook using online services like:
+4 -4
View File
@@ -409,8 +409,8 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
| Option | Default | Description |
| ---------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `--wait` | | Wait for services to start before returning. Implies --detach |
| `--base-image TEXT` | `langchain/langgraph-api` | Base image to use for the LangGraph API server. Pin to specific versions using version tags. |
| `--image TEXT` | | Docker image to use for the langgraph-api service. If specified, skips building and uses this image directly. |
| `--base-image TEXT` | `langchain/langgraph-api` | Base image to use for the LangGraph API server. Pin to specific versions using version tags. |
| `--image TEXT` | | Docker image to use for the langgraph-api service. If specified, skips building and uses this image directly. |
| `--postgres-uri TEXT` | Local database | Postgres URI to use for the database. |
| `--watch` | | Restart on file changes |
| `--debugger-base-url TEXT` | `http://127.0.0.1:[PORT]` | URL used by the debugger to access LangGraph API. |
@@ -438,8 +438,8 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
| Option | Default | Description |
| ---------------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| <span style="white-space: nowrap;">`--wait`</span> | | Wait for services to start before returning. Implies --detach |
| <span style="white-space: nowrap;">`--base-image TEXT`</span> | <span style="white-space: nowrap;">`langchain/langgraph-api`</span> | Base image to use for the LangGraph API server. Pin to specific versions using version tags. |
| <span style="white-space: nowrap;">`--image TEXT`</span> | | Docker image to use for the langgraph-api service. If specified, skips building and uses this image directly. |
| <span style="white-space: nowrap;">`--base-image TEXT`</span> | <span style="white-space: nowrap;">`langchain/langgraph-api`</span> | Base image to use for the LangGraph API server. Pin to specific versions using version tags. |
| <span style="white-space: nowrap;">`--image TEXT`</span> | | Docker image to use for the langgraph-api service. If specified, skips building and uses this image directly. |
| <span style="white-space: nowrap;">`--postgres-uri TEXT`</span> | Local database | Postgres URI to use for the database. |
| <span style="white-space: nowrap;">`--watch`</span> | | Restart on file changes |
| <span style="white-space: nowrap;">`-c, --config FILE`</span> | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
-3
View File
@@ -28,9 +28,6 @@ Specify `DD_API_KEY` (your [Datadog API Key](https://docs.datadoghq.com/account_
If `DD_API_KEY` is specified, the application process is wrapped in the [`ddtrace-run` command](https://ddtrace.readthedocs.io/en/stable/installation_quickstart.html). Other `DD_*` environment variables (e.g. `DD_SITE`, `DD_ENV`, `DD_SERVICE`, `DD_TRACE_ENABLED`) are typically needed to properly configure the tracing instrumentation. See [`DD_*` environment variables](https://ddtrace.readthedocs.io/en/stable/configuration.html) for more details.
!!! note
Enabling `DD_API_KEY` (and thus `ddtrace-run`) can override or interfere with other auto-instrumentation solutions (such as OpenTelemetry) that you may have instrumented into your application code.
## `LANGCHAIN_TRACING_SAMPLING_RATE`
Sampling rate for traces sent to LangSmith. Valid values: Any float between `0` and `1`.
@@ -4,67 +4,6 @@
---
## v0.2.103 (2025-07-25)
- Corrected the metadata endpoint to ensure accurate data retrieval.
## v0.2.102 (2025-07-24)
- Captured interrupt events in the wait method to preserve legacy behavior and stream updates by default.
- Added support for SDK structlog in the JavaScript environment, enhancing logging capabilities.
## v0.2.101 (2025-07-24)
- Used the correct metadata endpoint for self-hosted environments, resolving an access issue.
## v0.2.99 (2025-07-22)
- Improved license validation by adding an in-memory cache and handling Redis connection errors more effectively.
- Automatically remove agents from memory that are removed from `langgraph.json` to prevent persistence issues.
- Ensured the UI namespace for generated UI is a valid JavaScript property name to prevent errors.
- Raised a 422 error for improved request validation feedback.
## v0.2.98 (2025-07-19)
- Added langgraph node context for improved log filtering and trace visibility.
## v0.2.97 (2025-07-19)
- Fixed scheduling issue with ckpt ingestion worker that occurred on isolated background loops.
- Ensured queue worker starts only after all migrations have completed.
- Added more detailed error messages for thread state issues and improved response handling when state updates fail.
- Exposed interrupt ID while retrieving thread state for enhanced API response details.
## v0.2.96 (2025-07-17)
- Added a fallback mechanism for configurable header patterns to handle exclude/include settings more effectively.
## v0.2.95 (2025-07-17)
- Avoided setting the future if it is already done to prevent redundant operations.
- Resolved compatibility errors in CI by switching from `typing.TypedDict` to `typing_extensions.TypedDict` for Python versions below 3.12.
## v0.2.94 (2025-07-16)
- Improved performance by omitting pending sends for langgraph versions 0.5 and above.
- Improved server startup logs to provide clearer warnings when the DD_API_KEY environment variable is set.
## v0.2.93 (2025-07-16)
- Removed the GIN index for run metadata to improve performance.
## v0.2.92 (2025-07-16)
- Enabled copying functionality for blobs and checkpoints, improving data management flexibility.
## v0.2.91 (2025-07-16)
- Reduced writes to the `checkpoint_blobs` table by inlining small values (null, numeric, str, etc.). This means we don't need to store extra values for channels that haven't been updated.
## v0.2.90 (2025-07-16)
- Improve checkpoint writes via node-local background queueing.
## v0.2.89 (2025-07-15)
- Decoupled checkpoint writing from thread/run state by removing foreign keys and updated logger to prevent timeout-related failures.
## v0.2.88 (2025-07-14)
- Removed the foreign key constraint for `thread` in the `run` table to simplify database schema.
## v0.2.87 (2025-07-14)
- Added more detailed logs for Redis worker signaling to improve debugging.
## v0.2.86 (2025-07-11)
- Honored tool descriptions in the `/mcp` endpoint to align with expected functionality.
## v0.2.85 (2025-07-10)
- Added support for the `on_disconnect` field to `runs/wait` and included disconnect logs for better debugging.
+4 -4
View File
@@ -1,6 +1,6 @@
# Assistants
**Assistants** allow you to manage configurations (like prompts, LLM selection, tools) separately from your graph's core logic, enabling rapid changes that don't alter the graph architecture. It is a way to create multiple specialized versions of the same graph architecture, each optimized for different use cases through context/configuration variations rather than structural changes.
**Assistants** allow you to manage configurations (like prompts, LLM selection, tools) separately from your graph's core logic, enabling rapid changes that don't alter the graph architecture. It is a way to create multiple specialized versions of the same graph architecture, each optimized for different use cases through configuration variations rather than structural changes.
For example, imagine a general-purpose writing agent built on a common graph architecture. While the structure remains the same, different writing styles—such as blog posts and tweets—require tailored configurations to optimize performance. To support these variations, you can create multiple assistants (e.g., one for blogs and another for tweets) that share the underlying graph but differ in model selection and system prompt.
@@ -14,8 +14,8 @@ The LangGraph Cloud API provides several endpoints for creating and managing ass
## Configuration
Assistants build on the LangGraph open source concepts of configuration and [runtime context](low_level.md#runtime-context).
While these features are available in the open source LangGraph library, assistants are only present in [LangGraph Platform](langgraph_platform.md). This is due to the fact that assistants are tightly coupled to your deployed graph. Upon deployment, LangGraph Server will automatically create a default assistant for each graph using the graph's default context and configuration settings.
Assistants build on the LangGraph open source concept of [configuration](low_level.md#configuration).
While configuration is available in the open source LangGraph library, assistants are only present in [LangGraph Platform](langgraph_platform.md). This is due to the fact that assistants are tightly coupled to your deployed graph. Upon deployment, LangGraph Server will automatically create a default assistant for each graph using the graph's default configuration settings.
In practice, an assistant is just an _instance_ of a graph with a specific configuration. Therefore, multiple assistants can reference the same graph but can contain different configurations (e.g. prompts, models, tools). The LangGraph Server API provides several endpoints for creating and managing assistants. See the [API reference](../cloud/reference/api/api_ref.html) and [this how-to](../cloud/how-tos/configuration_cloud.md) for more details on how to create assistants.
@@ -26,6 +26,6 @@ Once you've created an assistant, subsequent edits to that assistant will create
## Execution
A **run** is an invocation of an assistant. Each run may have its own input, configuration, context, and metadata, which may affect execution and output of the underlying graph. A run can optionally be executed on a [thread](./persistence.md#threads).
A **run** is an invocation of an assistant. Each run may have its own input, configuration, and metadata, which may affect execution and output of the underlying graph. A run can optionally be executed on a [thread](./persistence.md#threads).
The LangGraph Platform API provides several endpoints for creating and managing runs. See the [API reference](../cloud/reference/api/api_ref.html#tag/thread-runs/) for more details.
+18
View File
@@ -0,0 +1,18 @@
---
search:
boost: 2
---
# Breakpoints
[Breakpoints](../how-tos/human_in_the_loop/breakpoints.md) pause graph execution at defined points and let you step through each stage. They use LangGraph's [**persistence layer**](./persistence.md), which saves the graph state after each step.
With breakpoints, you can inspect the graph's state and node inputs at any point. Execution pauses **indefinitely** until you resume, as the checkpointer preserves the state.
<figure markdown="1">
![image](img/breakpoints.png){: style="max-height:400px"}
<figcaption>An example graph consisting of 3 sequential steps with a breakpoint before step_3. </figcaption> </figure>
!!! tip
For information on how to use breakpoints, see [Set breakpoints](../how-tos/human_in_the_loop/breakpoints.md) and [Set breakpoints using Server API](../cloud/how-tos/human_in_the_loop_breakpoint.md).
+1 -1
View File
@@ -10,7 +10,7 @@ search:
There are two free options for deploying LangGraph applications via the LangGraph Server:
1. [Local](../tutorials/langgraph-platform/local-server.md): Deploy for local testing and development.
1. [Standalone Container (Lite)](../concepts/langgraph_standalone_container.md): A limited version of Standalone Container for deployments unlikely to see more than 1 million node executions per year and that do not need crons and other enterprise features. Standalone Container (Lite) deployment option is free with a LangSmith API key.
1. [Standalone Container (Lite)](../concepts/langgraph_standalone_container.md): A limited version of Standalone Container for deployments unlikely to see more that 1 million node executions per year and that do not need crons and other enterprise features. Standalone Container (Lite) deployment option is free with a LangSmith API key.
## Production deployment
+4 -4
View File
@@ -48,7 +48,7 @@ If a [node](./low_level.md#nodes) contains multiple operations, you may find it
from typing_extensions import TypedDict
import uuid
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
import requests
@@ -74,7 +74,7 @@ If a [node](./low_level.md#nodes) contains multiple operations, you may find it
builder.add_edge("call_api", END)
# Specify a checkpointer
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
# Compile the graph with the checkpointer
graph = builder.compile(checkpointer=checkpointer)
@@ -94,7 +94,7 @@ If a [node](./low_level.md#nodes) contains multiple operations, you may find it
from typing_extensions import TypedDict
import uuid
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import task
from langgraph.graph import StateGraph, START, END
import requests
@@ -129,7 +129,7 @@ If a [node](./low_level.md#nodes) contains multiple operations, you may find it
builder.add_edge("call_api", END)
# Specify a checkpointer
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
# Compile the graph with the checkpointer
graph = builder.compile(checkpointer=checkpointer)
+30 -33
View File
@@ -39,7 +39,7 @@ Here are some key differences:
Below we demonstrate a simple application that writes an essay and [interrupts](human_in_the_loop.md) to request human review.
```python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import interrupt
@@ -50,7 +50,7 @@ def write_essay(topic: str) -> str:
time.sleep(1) # A placeholder for a long-running task.
return f"An essay about topic: {topic}"
@entrypoint(checkpointer=InMemorySaver())
@entrypoint(checkpointer=MemorySaver())
def workflow(topic: str) -> dict:
"""A simple workflow that writes an essay and asks for a review."""
essay = write_essay("cat").result()
@@ -79,54 +79,51 @@ def workflow(topic: str) -> dict:
```python
import time
import uuid
from langgraph.func import entrypoint, task
from langgraph.types import interrupt
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
@task
def write_essay(topic: str) -> str:
"""Write an essay about the given topic."""
time.sleep(1) # This is a placeholder for a long-running task.
time.sleep(1) # This is a placeholder for a long-running task.
return f"An essay about topic: {topic}"
@entrypoint(checkpointer=InMemorySaver())
@entrypoint(checkpointer=MemorySaver())
def workflow(topic: str) -> dict:
"""A simple workflow that writes an essay and asks for a review."""
essay = write_essay("cat").result()
is_approved = interrupt(
{
# Any json-serializable payload provided to interrupt as argument.
# It will be surfaced on the client side as an Interrupt when streaming data
# from the workflow.
"essay": essay, # The essay we want reviewed.
# We can add any additional information that we need.
# For example, introduce a key called "action" with some instructions.
"action": "Please approve/reject the essay",
}
)
is_approved = interrupt({
# Any json-serializable payload provided to interrupt as argument.
# It will be surfaced on the client side as an Interrupt when streaming data
# from the workflow.
"essay": essay, # The essay we want reviewed.
# We can add any additional information that we need.
# For example, introduce a key called "action" with some instructions.
"action": "Please approve/reject the essay",
})
return {
"essay": essay, # The essay that was generated
"is_approved": is_approved, # Response from HIL
"essay": essay, # The essay that was generated
"is_approved": is_approved, # Response from HIL
}
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
config = {
"configurable": {
"thread_id": thread_id
}
}
for item in workflow.stream("cat", config):
print(item)
# > {'write_essay': 'An essay about topic: cat'}
# > {
# > '__interrupt__': (
# > Interrupt(
# > value={
# > 'essay': 'An essay about topic: cat',
# > 'action': 'Please approve/reject the essay'
# > },
# > id='b9b2b9d788f482663ced6dc755c9e981'
# > ),
# > )
# > }
```
```pycon
{'write_essay': 'An essay about topic: cat'}
{'__interrupt__': (Interrupt(value={'essay': 'An essay about topic: cat', 'action': 'Please approve/reject the essay'}, resumable=True, ns=['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], when='during'),)}
```
An essay has been written and is ready for review. Once the review is provided, we can resume the workflow:
+2 -11
View File
@@ -23,18 +23,9 @@ To review, edit, and approve tool calls in an agent or workflow, [use LangGraph'
## Key capabilities
* **Persistent execution state**: Interrupts use LangGraph's [persistence](./persistence.md) layer, which saves the graph state, to indefinitely pause graph execution until you resume. This is possible because LangGraph checkpoints the graph state after each step, which allows the system to persist execution context and later resume the workflow, continuing from where it left off. This supports asynchronous human review or input without time constraints.
* **Persistent execution state**: LangGraph allows you to pause execution **indefinitely** — for minutes, hours, or even days—until human input is received. This is possible because LangGraph checkpoints the graph state after each step, which allows the system to persist execution context and later resume the workflow, continuing from where it left off. This supports asynchronous human review or input without time constraints.
There are two ways to pause a graph:
- [Dynamic interrupts](../how-tos/human_in_the_loop/add-human-in-the-loop.md#pause-using-interrupt): Use `interrupt` to pause a graph from inside a specific node, based on the current state of the graph.
- [Static interrupts](../how-tos/human_in_the_loop/add-human-in-the-loop.md#debug-with-interrupts): Use `interrupt_before` and `interrupt_after` to pause the graph at defined points, either before or after a node executes.
<figure markdown="1">
![image](./img/breakpoints.png){: style="max-height:400px"}
<figcaption>An example graph consisting of 3 sequential steps with a breakpoint before step_3. </figcaption> </figure>
* **Flexible integration points**: Human-in-the-loop logic can be introduced at any point in the workflow. This allows targeted human involvement, such as approving API calls, correcting outputs, or guiding conversations.
* **Flexible integration points**: HIL logic can be introduced at any point in the workflow. This allows targeted human involvement, such as approving API calls, correcting outputs, or guiding conversations.
## Patterns
Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

@@ -119,11 +119,6 @@ These metrics are displayed as charts in the Control Plane UI.
### LangSmith Integration
A [LangSmith](https://docs.smith.langchain.com/) tracing project and LangSmith API key are automatically created for each deployment. The deployment uses the API key to automatically send traces to LangSmith.
A [LangSmith](https://docs.smith.langchain.com/) tracing project is automatically created for each deployment. The tracing project has the same name as the deployment. When creating a deployment, the `LANGCHAIN_TRACING` and `LANGSMITH_API_KEY`/`LANGCHAIN_API_KEY` environment variables do not need to be specified; they are set automatically by the control plane.
- The tracing project has the same name as the deployment.
- The API key has the description `LangGraph Platform: <deployment_name>`.
- The API key is never revealed and cannot be deleted manually.
- When creating a deployment, the `LANGCHAIN_TRACING` and `LANGSMITH_API_KEY`/`LANGCHAIN_API_KEY` environment variables do not need to be specified; they are set automatically by the control plane.
When a deployment is deleted, the traces and the tracing project are not deleted. However, the API will be deleted when the deployment is deleted.
When a deployment is deleted, the traces and the tracing project are not deleted.
+29 -41
View File
@@ -45,7 +45,7 @@ The first thing you do when you define a graph is define the `State` of the grap
### Schema
The main documented way to specify the schema of a graph is by using a [`TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict). If you want to provide default values in your state, use a [`dataclass`](https://docs.python.org/3/library/dataclasses.html). We also support using a Pydantic [BaseModel](../how-tos/graph-api.md#use-pydantic-models-for-graph-state) as your graph state if you want recursive data validation (though note that pydantic is less performant than a `TypedDict` or `dataclass`).
The main documented way to specify the schema of a graph is by using `TypedDict`. However, we also support [using a Pydantic BaseModel](../how-tos/graph-api.md#use-pydantic-models-for-graph-state) as your graph state to add **default values** and additional data validation.
By default, the graph will have the same input and output schemas. If you want to change this, you can also specify explicit input and output schemas directly. This is useful when you have a lot of keys, and some are explicitly for input and others for output. See the [guide here](../how-tos/graph-api.md#define-input-and-output-schemas) for how to use.
@@ -192,48 +192,35 @@ class State(MessagesState):
## Nodes
In LangGraph, nodes are Python functions (either synchronous or asynchronous) that accept the following arguments:
1. `state`: The [state](#state) of the graph
2. `config`: A `RunnableConfig` object that contains configuration information like `thread_id` and tracing information like `tags`
3. `runtime`: A `Runtime` object that contains [runtime `context`](#runtime-context) and other information like `store` and `stream_writer`
In LangGraph, nodes are typically python functions (sync or async) where the **first** positional argument is the [state](#state), and (optionally), the **second** positional argument is a "config", containing optional [configurable parameters](#configuration) (such as a `thread_id`).
Similar to `NetworkX`, you add these nodes to a graph using the [add_node][langgraph.graph.StateGraph.add_node] method:
```python
from dataclasses import dataclass
from typing_extensions import TypedDict
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph
from langgraph.runtime import Runtime
class State(TypedDict):
input: str
results: str
@dataclass
class Context:
user_id: str
builder = StateGraph(State)
def plain_node(state: State):
def my_node(state: State, config: RunnableConfig):
print("In node: ", config["configurable"]["user_id"])
return {"results": f"Hello, {state['input']}!"}
# The second argument is optional
def my_other_node(state: State):
return state
def node_with_runtime(state: State, runtime: Runtime[Context]):
print("In node: ", runtime.context.user_id)
return {"results": f"Hello, {state['input']}!"}
def node_with_config(state: State, config: RunnableConfig):
print("In node with thread_id: ", config["configurable"]["thread_id"])
return {"results": f"Hello, {state['input']}!"}
builder.add_node("plain_node", plain_node)
builder.add_node("node_with_runtime", node_with_runtime)
builder.add_node("node_with_config", node_with_config)
builder.add_node("my_node", my_node)
builder.add_node("other_node", my_other_node)
...
```
@@ -311,7 +298,7 @@ print(graph.invoke({"x": 5}, stream_mode='updates')) # (2)!
[{'expensive_node': {'result': 10}, '__metadata__': {'cached': True}}]
```
1. First run takes two seconds to run (due to mocked expensive computation).
1. First run takes the full second to run (due to mocked expensive computation).
2. Second run utilizes cache and returns quickly.
## Edges
@@ -472,32 +459,33 @@ LangGraph can easily handle migrations of graph definitions (nodes, edges, and s
- State keys that are renamed lose their saved state in existing threads
- State keys whose types change in incompatible ways could currently cause issues in threads with state from before the change -- if this is a blocker please reach out and we can prioritize a solution.
## Runtime Context
## Configuration
When creating a graph, you can specify a `context_schema` for runtime context passed to nodes. This is useful for passing
information to nodes that is not part of the graph state. For example, you might want to pass dependencies such as model name or a database connection.
When creating a graph, you can also mark that certain parts of the graph are configurable. This is commonly done to enable easily switching between models or system prompts. This allows you to create a single "cognitive architecture" (the graph) but have multiple different instance of it.
You can optionally specify a `config_schema` when creating a graph.
```python
@dataclass
class ContextSchema:
llm_provider: str = "openai"
class ConfigSchema(TypedDict):
llm: str
graph = StateGraph(State, context_schema=ContextSchema)
graph = StateGraph(State, config_schema=ConfigSchema)
```
You can then pass this context into the graph using the `context` parameter of the `invoke` method.
You can then pass this configuration into the graph using the `configurable` config field.
```python
graph.invoke(inputs, context={"llm_provider": "anthropic"})
config = {"configurable": {"llm": "anthropic"}}
graph.invoke(inputs, config=config)
```
You can then access and use this context inside a node or conditional edge:
You can then access and use this configuration inside a node or conditional edge:
```python
from langgraph.runtime import Runtime
def node_a(state: State, runtime: Runtime[ContextSchema]):
llm = get_llm(runtime.context.llm_provider)
def node_a(state, config):
llm_type = config.get("configurable", {}).get("llm", "openai")
llm = get_llm(llm_type)
...
```
@@ -508,7 +496,7 @@ See [this guide](../how-tos/graph-api.md#add-runtime-configuration) for a full b
The recursion limit sets the maximum number of [super-steps](#graphs) the graph can execute during a single execution. Once the limit is reached, LangGraph will raise `GraphRecursionError`. By default this value is set to 25 steps. The recursion limit can be set on any graph at runtime, and is passed to `.invoke`/`.stream` via the config dictionary. Importantly, `recursion_limit` is a standalone `config` key and should not be passed inside the `configurable` key as all other user-defined configuration. See the example below:
```python
graph.invoke(inputs, config={"recursion_limit": 5}, context={"llm": "anthropic"})
graph.invoke(inputs, config={"recursion_limit": 5, "configurable":{"llm": "anthropic"}})
```
Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/) to learn more about how the recursion limit works.
+4 -4
View File
@@ -33,7 +33,7 @@ The state of a thread at a particular point in time is called a checkpoint. Chec
- `metadata`: Metadata associated with this checkpoint.
- `values`: Values of the state channels at this point in time.
- `next` A tuple of the node names to execute next in the graph.
- `tasks`: A tuple of `PregelTask` objects that contain information about next tasks to be executed. If the step was previously attempted, it will include error information. If a graph was interrupted [dynamically](../how-tos/human_in_the_loop/add-human-in-the-loop.md#pause-using-interrupt) from within a node, tasks will contain additional data associated with interrupts.
- `tasks`: A tuple of `PregelTask` objects that contain information about next tasks to be executed. If the step was previously attempted, it will include error information. If a graph was interrupted [dynamically](../how-tos/human_in_the_loop/breakpoints.md#dynamic-breakpoints) from within a node, tasks will contain additional data associated with interrupts.
Checkpoints are persisted and can be used to restore the state of a thread at a later time.
@@ -487,12 +487,12 @@ If you want to fallback to pickle for objects not currently supported by our msg
you can use the `pickle_fallback` argument of the `JsonPlusSerializer`:
```python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
# ... Define the graph ...
graph.compile(
checkpointer=InMemorySaver(serde=JsonPlusSerializer(pickle_fallback=True))
checkpointer=MemorySaver(serde=JsonPlusSerializer(pickle_fallback=True))
)
```
@@ -525,7 +525,7 @@ When running on LangGraph Platform, encryption is automatically enabled whenever
### Human-in-the-loop
First, checkpointers facilitate [human-in-the-loop workflows](agentic_concepts.md#human-in-the-loop) workflows by allowing humans to inspect, interrupt, and approve graph steps. Checkpointers are needed for these workflows as the human has to be able to view the state of a graph at any point in time, and the graph has to be to resume execution after the human has made any updates to the state. See [the how-to guides](../how-tos/human_in_the_loop/add-human-in-the-loop.md) for examples.
First, checkpointers facilitate [human-in-the-loop workflows](agentic_concepts.md#human-in-the-loop) workflows by allowing humans to inspect, interrupt, and approve graph steps. Checkpointers are needed for these workflows as the human has to be able to view the state of a graph at any point in time, and the graph has to be to resume execution after the human has made any updates to the state. See [these how-to guides](../how-tos/human_in_the_loop/breakpoints.md) for concrete examples.
### Memory
-17
View File
@@ -1,17 +0,0 @@
# Tracing
Traces are a series of steps that your application takes to go from input to output. Each of these individual steps is represented by a run. You can use [LangSmith](https://smith.langchain.com/) to visualize these execution steps. To use it, [enable tracing for your application](../how-tos/enable-tracing.md). This enables you to do the following:
- [Debug a locally running application](../cloud/how-tos/clone_traces_studio.md).
- [Evaluate the application performance](../agents/evals.md).
- [Monitor the application](https://docs.smith.langchain.com/observability/how_to_guides/dashboards).
To get started, sign up for a free account at [LangSmith](https://smith.langchain.com/).
## Learn more
- [Graph runs in LangSmith](../how-tos/run-id-langsmith.md)
- [LangSmith Observability quickstart](https://docs.smith.langchain.com/observability)
- [Trace with LangGraph](https://docs.smith.langchain.com/observability/how_to_guides/trace_with_langgraph)
- [Tracing conceptual guide](https://docs.smith.langchain.com/observability/concepts#traces)
+4 -8
View File
@@ -2,11 +2,6 @@
The pages in this section provide a conceptual overview and how-tos for the following topics:
## Agent development
- [Overview](../agents/overview.md): Use prebuilt components to build an agent.
- [Run an agent](../agents/run_agents.md): Run an agent by providing input, interpreting output, enabling streaming, and controlling execution limits.
## LangGraph APIs
- [Graph API](../concepts/low_level.md): Use the Graph API to define workflows using a graph paradigm.
@@ -24,7 +19,8 @@ These capabilities are available in both LangGraph OSS and the LangGraph Platfor
- [Context](../agents/context.md): Pass outside data to a LangGraph graph to provide context for the graph execution.
- [Models](../agents/models.md): Integrate various LLMs into your LangGraph application.
- [Tools](../concepts/tools.md): Interface directly with external systems.
- [Human-in-the-loop](../concepts/human_in_the_loop.md): Pause a graph and wait for human input at any point in a workflow.
- [Human-in-the-loop](../concepts/human_in_the_loop.md): Enable human intervention at any point in a workflow.
- [Breakpoints](../concepts/breakpoints.md): Pause the execution of a LangGraph graph at a specific point.
- [Time travel](../concepts/time-travel.md): Travel back in time to a specific point in the execution of a LangGraph graph.
- [Subgraphs](../concepts/subgraphs.md): Build modular graphs.
- [Multi-agent](../concepts/multi_agent.md): Break down a complex workflow into multiple agents.
@@ -35,11 +31,11 @@ These capabilities are available in both LangGraph OSS and the LangGraph Platfor
These capabilities are only available in [LangGraph Platform](../concepts/langgraph_platform.md).
- [Authentication and access control](../concepts/auth.md): Authenticate and authorize users to access a LangGraph graph.
- [Authentication and access control](../concepts/auth.md): Authenticate and authorize users to access a Langraph graph.
- [Assistants](../concepts/assistants.md): Build assistants that can be used to interact with a LangGraph graph.
- [Double-texting](../concepts/double_texting.md): Handle double-texting (consecutive messages before a first response is returned) in a LangGraph graph.
- [Webhooks](../cloud/concepts/webhooks.md): Send webhooks to a LangGraph graph.
- [Cron jobs](../cloud/concepts/cron_jobs.md): Schedule jobs to run at a specific time.
- [Server customization](../how-tos/http/custom_lifespan.md): Customize the server that runs a LangGraph graph.
- [Data management](../cloud/concepts/data_storage_and_privacy.md): Manage data in a LangGraph graph.
- [Deployment](../concepts/deployment_options.md): Deploy a LangGraph graph to a server.
- [Deployment](../concepts/deployment_options.md): Deploy a LangGraph graph to a server.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

+14 -43
View File
@@ -31,12 +31,12 @@ To leverage custom authentication and access user-level metadata in your deploym
api_key = headers.get("x-api-key")
if not api_key or not is_valid_key(api_key):
raise Auth.exceptions.HTTPException(status_code=401, detail="Invalid API key")
# Fetch user-specific tokens from your secret store
# Fetch user-specific tokens from your secret store
user_tokens = await fetch_user_tokens(api_key)
return { # (2)!
"identity": api_key, # fetch user ID from LangSmith
"identity": api_key, # fetch user ID from LangSmith
"github_token" : user_tokens.github_token
"jira_token" : user_tokens.jira_token
# ... custom fields/secrets here
@@ -50,14 +50,14 @@ To leverage custom authentication and access user-level metadata in your deploym
```json hl_lines="7-9"
{
"dependencies": ["."],
"graphs": {
"dependencies": ["."],
"graphs": {
"agent": "./agent.py:graph"
},
"env": ".env",
"auth": {
},
"env": ".env",
"auth": {
"path": "./auth.py:my_auth"
}
}
}
```
@@ -80,7 +80,7 @@ To leverage custom authentication and access user-level metadata in your deploym
```python
from langgraph.pregel.remote import RemoteGraph
my_token = "your-token" # In practice, you would generate a signed token with your auth provider
remote_graph = RemoteGraph(
"agent",
@@ -133,44 +133,15 @@ To allow an agent to perform authenticated actions on behalf of the user, access
def my_node(state, config):
user_config = config["configurable"].get("langgraph_auth_user")
# token was resolved during the @auth.authenticate function
token = user_config.get("github_token","")
token = user_config.get("github_token","")
...
```
!!! note
Fetch user credentials from a secure secret store. Storing secrets in graph state is not recommended.
### Authorizing a Studio user
By default, if you add custom authorization on your resources, this will also apply to interactions made from the Studio. If you want, you can handle logged-in Studio users differently by checking [is_studio_user()](../../reference/functions/sdk_auth.isStudioUser.html).
!!! note
`is_studio_user` was added in version 0.1.73 of the langgraph-sdk. If you're on an older version, you can still check whether `isinstance(ctx.user, StudioUser)`.
```python
from langgraph_sdk.auth import is_studio_user, Auth
auth = Auth()
# ... Setup authenticate, etc.
@auth.on
async def add_owner(
ctx: Auth.types.AuthContext,
value: dict # The payload being sent to this access method
) -> dict: # Returns a filter dict that restricts access to resources
if is_studio_user(ctx.user):
return {}
filters = {"owner": ctx.user.identity}
metadata = value.setdefault("metadata", {})
metadata.update(filters)
return filters
```
Only use this if you want to permit developer access to a graph deployed on the managed LangGraph Platform SaaS.
## Learn more
- [Authentication & Access Control](../../concepts/auth.md)
- [LangGraph Platform](../../concepts/langgraph_platform.md)
- [Setting up custom authentication tutorial](../../tutorials/auth/getting_started.md)
* [Authentication & Access Control](../../concepts/auth.md)
* [LangGraph Platform](../../concepts/langgraph_platform.md)
* [Setting up custom authentication tutorial](../../tutorials/auth/getting_started.md)
@@ -77,7 +77,7 @@
"metadata": {},
"outputs": [
{
"name": "stdout",
"name": "stdin",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
@@ -165,7 +165,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 8,
"id": "d129e4e1-3766-429a-b806-cde3d8bc0469",
"metadata": {},
"outputs": [],
@@ -173,7 +173,7 @@
"from langchain_core.messages import convert_to_openai_messages, BaseMessage\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.graph import add_messages\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"\n",
"@task\n",
@@ -192,7 +192,7 @@
"\n",
"\n",
"# add short-term memory for storing conversation history\n",
"checkpointer = InMemorySaver()\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
@@ -222,12 +222,12 @@
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"Find numbers between 10 and 30 in fibonacci sequence\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
"\n",
@@ -253,9 +253,9 @@
"This script will print the Fibonacci numbers between 10 and 30. Please execute the code to see the result.\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[31m\n",
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n",
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
"\u001B[31m\n",
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001B[0m\n",
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"exitcode: 0 (execution succeeded)\n",
"Code output: \n",
@@ -264,7 +264,7 @@
"\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
"\n",
@@ -318,7 +318,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
"\n",
"Multiply the last number by 3\n",
"Context: \n",
@@ -334,7 +334,7 @@
"TERMINATE\n",
"\n",
"--------------------------------------------------------------------------------\n",
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
"\n",
"The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n",
"\n",
+5 -5
View File
@@ -75,7 +75,7 @@ We will now create a LangGraph chatbot graph that calls AutoGen agent.
```python
from langchain_core.messages import convert_to_openai_messages
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
def call_autogen_agent(state: MessagesState):
# Convert LangGraph messages to OpenAI format for AutoGen
@@ -101,7 +101,7 @@ def call_autogen_agent(state: MessagesState):
return {"messages": {"role": "assistant", "content": final_content}}
# Create the graph with memory for persistence
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
# Build the graph
builder = StateGraph(MessagesState)
@@ -228,7 +228,7 @@ my-autogen-agent/
import autogen
from langchain_core.messages import convert_to_openai_messages
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
# AutoGen configuration
config_list = [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}]
@@ -276,7 +276,7 @@ my-autogen-agent/
# Create and compile the graph
def create_graph():
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
builder = StateGraph(MessagesState)
builder.add_node("autogen", call_autogen_agent)
builder.add_edge(START, "autogen")
@@ -290,7 +290,7 @@ my-autogen-agent/
```
langgraph>=0.1.0
ag2>=0.2.0
pyautogen>=0.2.0
langchain-core>=0.1.0
langchain-openai>=0.0.5
```
@@ -167,7 +167,7 @@
"from langchain_core.messages import BaseMessage\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.graph import add_messages\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.store.base import BaseStore\n",
"\n",
"\n",
@@ -192,7 +192,7 @@
"\n",
"\n",
"# NOTE: we're passing the store object here when creating a workflow via entrypoint()\n",
"@entrypoint(checkpointer=InMemorySaver(), store=in_memory_store)\n",
"@entrypoint(checkpointer=MemorySaver(), store=in_memory_store)\n",
"def workflow(\n",
" inputs: list[BaseMessage],\n",
" *,\n",
-16
View File
@@ -1,16 +0,0 @@
# Enable tracing for your application
To enable [tracing](../concepts/tracing.md) for your application, set the following environment variables:
```python
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=<your-api-key>
```
For more information, see [Trace with LangGraph](https://docs.smith.langchain.com/observability/how_to_guides/trace_with_langgraph).
## Learn more
- [Graph runs in LangSmith](../how-tos/run-id-langsmith.md)
- [LangSmith Observability quickstart](https://docs.smith.langchain.com/observability)
- [Tracing conceptual guide](https://docs.smith.langchain.com/observability/concepts#traces)
+40 -43
View File
@@ -328,15 +328,14 @@ Output of graph invocation: {'a': 'set by node_3'}
A [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.StateGraph) accepts a `state_schema` argument on initialization that specifies the "shape" of the state that the nodes in the graph can access and update.
In our examples, we typically use a python-native `TypedDict` or [`dataclass`](https://docs.python.org/3/library/dataclasses.html) for `state_schema`, but `state_schema` can be any [type](https://docs.python.org/3/library/stdtypes.html#type-objects).
In our examples, we typically use a python-native `TypedDict` for `state_schema`, but `state_schema` can be any [type](https://docs.python.org/3/library/stdtypes.html#type-objects).
Here, we'll see how a [Pydantic BaseModel](https://docs.pydantic.dev/latest/api/base_model/) can be used for `state_schema` to add run-time validation on **inputs**.
Here, we'll see how a [Pydantic BaseModel](https://docs.pydantic.dev/latest/api/base_model/). can be used for `state_schema` to add run time validation on **inputs**.
!!! note "Known Limitations"
- Currently, the output of the graph will **NOT** be an instance of a pydantic model.
- Run-time validation only occurs on inputs into nodes, not on the outputs.
- The validation error trace from pydantic does not show which node the error arises in.
- Pydantic's recursive validation can be slow. For performance-sensitive applications, you may want to consider using a `dataclass` instead.
```python
from langgraph.graph import StateGraph, START, END
@@ -514,12 +513,12 @@ To add runtime configuration:
See below for a simple example:
```python
from langchain_core.runnables import RunnableConfig
from langgraph.graph import END, StateGraph, START
from langgraph.runtime import Runtime
from typing_extensions import TypedDict
# 1. Specify config schema
class ContextSchema(TypedDict):
class ConfigSchema(TypedDict):
my_runtime_value: str
# 2. Define a graph that accesses the config in a node
@@ -527,18 +526,18 @@ class State(TypedDict):
my_state_value: str
# highlight-next-line
def node(state: State, runtime: Runtime[ContextSchema]):
def node(state: State, config: RunnableConfig):
# highlight-next-line
if runtime.context["my_runtime_value"] == "a":
if config["configurable"]["my_runtime_value"] == "a":
return {"my_state_value": 1}
# highlight-next-line
elif runtime.context["my_runtime_value"] == "b":
elif config["configurable"]["my_runtime_value"] == "b":
return {"my_state_value": 2}
else:
raise ValueError("Unknown values.")
# highlight-next-line
builder = StateGraph(State, context_schema=ContextSchema)
builder = StateGraph(State, config_schema=ConfigSchema)
builder.add_node(node)
builder.add_edge(START, "node")
builder.add_edge("node", END)
@@ -547,9 +546,9 @@ graph = builder.compile()
# 3. Pass in configuration at runtime:
# highlight-next-line
print(graph.invoke({}, context={"my_runtime_value": "a"}))
print(graph.invoke({}, {"configurable": {"my_runtime_value": "a"}}))
# highlight-next-line
print(graph.invoke({}, context={"my_runtime_value": "b"}))
print(graph.invoke({}, {"configurable": {"my_runtime_value": "b"}}))
```
```
{'my_state_value': 1}
@@ -560,28 +559,27 @@ print(graph.invoke({}, context={"my_runtime_value": "b"}))
Below we demonstrate a practical example in which we configure what LLM to use at runtime. We will use both OpenAI and Anthropic models.
```python
from dataclasses import dataclass
from langchain.chat_models import init_chat_model
from langgraph.graph import MessagesState, END, StateGraph, START
from langgraph.runtime import Runtime
from langchain_core.runnables import RunnableConfig
from langgraph.graph import MessagesState
from langgraph.graph import END, StateGraph, START
from typing_extensions import TypedDict
@dataclass
class ContextSchema:
model_provider: str = "anthropic"
class ConfigSchema(TypedDict):
model: str
MODELS = {
"anthropic": init_chat_model("anthropic:claude-3-5-haiku-latest"),
"openai": init_chat_model("openai:gpt-4.1-mini"),
}
def call_model(state: MessagesState, runtime: Runtime[ContextSchema]):
model = MODELS[runtime.context.model_provider]
def call_model(state: MessagesState, config: RunnableConfig):
model = config["configurable"].get("model", "anthropic")
model = MODELS[model]
response = model.invoke(state["messages"])
return {"messages": [response]}
builder = StateGraph(MessagesState, context_schema=ContextSchema)
builder = StateGraph(MessagesState, config_schema=ConfigSchema)
builder.add_node("model", call_model)
builder.add_edge(START, "model")
builder.add_edge("model", END)
@@ -593,7 +591,8 @@ print(graph.invoke({}, context={"my_runtime_value": "b"}))
# With no configuration, uses default (Anthropic)
response_1 = graph.invoke({"messages": [input_message]})["messages"][-1]
# Or, can set OpenAI
response_2 = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai"})["messages"][-1]
config = {"configurable": {"model": "openai"}}
response_2 = graph.invoke({"messages": [input_message]}, config=config)["messages"][-1]
print(response_1.response_metadata["model_name"])
print(response_2.response_metadata["model_name"])
@@ -607,33 +606,32 @@ print(graph.invoke({}, context={"my_runtime_value": "b"}))
Below we demonstrate a practical example in which we configure two parameters: the LLM and system message to use at runtime.
```python
from dataclasses import dataclass
from typing import Optional
from langchain.chat_models import init_chat_model
from langchain_core.messages import SystemMessage
from langchain_core.runnables import RunnableConfig
from langgraph.graph import END, MessagesState, StateGraph, START
from langgraph.runtime import Runtime
from typing_extensions import TypedDict
@dataclass
class ContextSchema:
model_provider: str = "anthropic"
system_message: str | None = None
class ConfigSchema(TypedDict):
model: Optional[str]
system_message: Optional[str]
MODELS = {
"anthropic": init_chat_model("anthropic:claude-3-5-haiku-latest"),
"openai": init_chat_model("openai:gpt-4.1-mini"),
}
def call_model(state: MessagesState, runtime: Runtime[ContextSchema]):
model = MODELS[runtime.context.model_provider]
def call_model(state: MessagesState, config: RunnableConfig):
model = config["configurable"].get("model", "anthropic")
model = MODELS[model]
messages = state["messages"]
if (system_message := runtime.context.system_message):
if system_message := config["configurable"].get("system_message"):
messages = [SystemMessage(system_message)] + messages
response = model.invoke(messages)
return {"messages": [response]}
builder = StateGraph(MessagesState, context_schema=ContextSchema)
builder = StateGraph(MessagesState, config_schema=ConfigSchema)
builder.add_node("model", call_model)
builder.add_edge(START, "model")
builder.add_edge("model", END)
@@ -642,7 +640,8 @@ print(graph.invoke({}, context={"my_runtime_value": "b"}))
# Usage
input_message = {"role": "user", "content": "hi"}
response = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai", "system_message": "Respond in Italian."})
config = {"configurable": {"model": "openai", "system_message": "Respond in Italian."}}
response = graph.invoke({"messages": [input_message]}, config)
for message in response["messages"]:
message.pretty_print()
```
@@ -1150,15 +1149,13 @@ Adding "C" to ['A']
LangGraph supports map-reduce and other advanced branching patterns using the Send API. Here is an example of how to use it:
```python
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
from typing_extensions import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, START, END, Send
from typing_extensions import TypedDict
class OverallState(TypedDict):
topic: str
subjects: list[str]
jokes: Annotated[list[str], operator.add]
jokes: list[str]
best_selected_joke: str
def generate_topics(state: OverallState):
@@ -1196,7 +1193,7 @@ from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))
```
![Map-reduce graph with fanout](assets/graph_api_image_6.png)
![Map-reduce graph with fanout](assets/graph_api_image_2.png)
```python
# Call the graph: here we call it to generate a list of jokes
@@ -1448,7 +1445,7 @@ Recursion Error
display(Image(graph.get_graph().draw_mermaid_png()))
```
![Complex loop graph with branches](assets/graph_api_image_8.png)
![Complex loop graph with branches](assets/graph_api_image_4.png)
This graph looks complex, but can be conceptualized as loop of [supersteps](../concepts/low_level.md#graphs):
@@ -1510,7 +1507,7 @@ Because many LangChain objects implement the [Runnable Protocol](https://python.
See example below. To demonstrate async invocations of underlying LLMs, we will include a chat model:
{% include-markdown "../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
```python
from langchain.chat_models import init_chat_model
@@ -1567,9 +1564,9 @@ class State(TypedDict):
def node_a(state: State) -> Command[Literal["node_b", "node_c"]]:
print("Called A")
value = random.choice(["b", "c"])
value = random.choice(["a", "b"])
# this is a replacement for a conditional edge function
if value == "b":
if value == "a":
goto = "node_b"
else:
goto = "node_c"
@@ -11,19 +11,11 @@ hide:
# Enable human intervention
To review, edit, and approve tool calls in an agent or workflow, use interrupts to pause a graph and wait for human input. Interrupts use LangGraph's [persistence](../../concepts/persistence.md) layer, which saves the graph state, to indefinitely pause graph execution until you resume.
!!! info
For more information about human-in-the-loop workflows, see the [Human-in-the-Loop](../../concepts/human_in_the_loop.md) conceptual guide.
To review, edit, and approve tool calls in an agent or workflow, use LangGraph's [human-in-the-loop](../../concepts/human_in_the_loop.md) features.
## Pause using `interrupt`
[Dynamic interrupts](../../concepts/human_in_the_loop.md#key-capabilities) (also known as dynamic breakpoints) are triggered based on the current state of the graph. You can set dynamic interrupts by calling [`interrupt` function][langgraph.types.interrupt] in the appropriate place. The graph will pause, which allows for human intervention, and then resumes the graph with their input. It's useful for tasks like approvals, edits, or gathering additional context.
!!! note
As of v1.0, `interrupt` is the recommended way to pause a graph. `NodeInterrupt` is deprecated and will be removed in v2.0.
The [`interrupt` function][langgraph.types.interrupt] in LangGraph enables human-in-the-loop workflows by pausing the graph at a specific node, presenting information to a human, and resuming the graph with their input. It's useful for tasks like approvals, edits, or gathering additional context.
To use `interrupt` in your graph, you need to:
@@ -54,7 +46,13 @@ graph = graph_builder.compile(checkpointer=checkpointer) # (4)!
config = {"configurable": {"thread_id": "some_id"}}
result = graph.invoke({"some_text": "original text"}, config=config) # (5)!
print(result['__interrupt__']) # (6)!
# > [Interrupt(value={'text_to_revise': 'original text'}, id='a0d9dd40440ac7be2720dc5c20858627')]
# > [
# > Interrupt(
# > value={'text_to_revise': 'original text'},
# > resumable=True,
# > ns=['human_node:6ce9e64f-edef-fe5d-f7dc-511fa9526960']
# > )
# > ]
# highlight-next-line
print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
@@ -74,27 +72,25 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
```python
from typing import TypedDict
import uuid
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START
from langgraph.graph import StateGraph
# highlight-next-line
from langgraph.types import interrupt, Command
class State(TypedDict):
some_text: str
def human_node(state: State):
# highlight-next-line
value = interrupt( # (1)!
value = interrupt( # (1)!
{
"text_to_revise": state["some_text"] # (2)!
"text_to_revise": state["some_text"] # (2)!
}
)
return {
"some_text": value # (3)!
"some_text": value # (3)!
}
@@ -102,15 +98,25 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
graph_builder = StateGraph(State)
graph_builder.add_node("human_node", human_node)
graph_builder.add_edge(START, "human_node")
checkpointer = InMemorySaver() # (4)!
checkpointer = InMemorySaver() # (4)!
graph = graph_builder.compile(checkpointer=checkpointer)
# Pass a thread ID to the graph to run it.
config = {"configurable": {"thread_id": uuid.uuid4()}}
# Run the graph until the interrupt is hit.
result = graph.invoke({"some_text": "original text"}, config=config) # (5)!
print(result["__interrupt__"]) # (6)!
# > [Interrupt(value={'text_to_revise': 'original text'}, id='6d7c4048049254c83195429a3659661d')]
# Run the graph until the interrupt is hit.
result = graph.invoke({"some_text": "original text"}, config=config) # (5)!
print(result['__interrupt__']) # (6)!
# > [
# > Interrupt(
# > value={'text_to_revise': 'original text'},
# > resumable=True,
# > ns=['human_node:6ce9e64f-edef-fe5d-f7dc-511fa9526960']
# > )
# > ]
# highlight-next-line
print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
@@ -132,10 +138,15 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
!!! warning
Interrupts resemble Python's input() function in terms of developer experience, but they do not automatically resume execution from the interruption point. Instead, they rerun the entire node where the interrupt was used. For this reason, interrupts are typically best placed at the start of a node or in a dedicated node.
Interrupts are both powerful and ergonomic. However, while they may resemble Python's input() function in terms of developer experience, it's important to note that they do not automatically resume execution from the interruption point. Instead, they rerun the entire node where the interrupt was used. For this reason, interrupts are typically best placed at the start of a node or in a dedicated node.
## Resume using the `Command` primitive
!!! warning
Resuming from an `interrupt` is different from Python's `input()` function, where execution resumes from the exact point where the `input()` function was called.
When the `interrupt` function is used within a graph, execution pauses at that point and awaits user input.
To resume execution, use the [`Command`][langgraph.types.Command] primitive, which can be supplied via the `invoke`, `ainvoke`, `stream`, or `astream` methods. The graph resumes execution from the beginning of the node where `interrupt(...)` was initially called. This time, the `interrupt` function will return the value provided in `Command(resume=value)` rather than pausing again. All code from the beginning of the node to the `interrupt` will be re-executed.
@@ -153,7 +164,7 @@ For example, once your graph has been interrupted (multiple times, theoretically
```python
resume_map = {
i.id: f"human input for prompt {i.value}"
i.interrupt_id: f"human input for prompt {i.value}"
for i in parent.get_state(thread_config).interrupts
}
@@ -212,7 +223,7 @@ graph.invoke(Command(resume=True), config=thread_config)
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
# Define the shared graph state
class State(TypedDict):
@@ -257,7 +268,7 @@ graph.invoke(Command(resume=True), config=thread_config)
builder.add_edge("approved_path", END)
builder.add_edge("rejected_path", END)
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# Run until interrupt
@@ -325,7 +336,7 @@ graph.invoke(
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
# Define the graph state
class State(TypedDict):
@@ -364,7 +375,7 @@ graph.invoke(
builder.add_edge("downstream_use", END)
# Set up in-memory checkpointing for interrupt support
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# Invoke the graph until it hits the interrupt
@@ -374,15 +385,14 @@ graph.invoke(
# Output interrupt payload
print(result["__interrupt__"])
# Example output:
# > [
# > Interrupt(
# > value={
# > 'task': 'Please review and edit the generated summary if necessary.',
# > 'generated_summary': 'The cat sat on the mat and looked at the stars.'
# > },
# > id='...'
# > )
# > ]
# Interrupt(
# value={
# 'task': 'Please review and edit the generated summary if necessary.',
# 'generated_summary': 'The cat sat on the mat and looked at the stars.'
# },
# resumable=True,
# ...
# )
# Resume the graph with human-edited input
edited_summary = "The cat lay on the rug, gazing peacefully at the night sky."
@@ -642,7 +652,7 @@ def human_node(state: State):
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
# Define graph state
class State(TypedDict):
@@ -681,7 +691,7 @@ def human_node(state: State):
builder.add_edge("report_age", END)
# Create the graph with a memory checkpointer
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# Run the graph until the first interrupt
@@ -702,162 +712,6 @@ def human_node(state: State):
print(final_result) # Should include the valid age
```
## Debug with interrupts
To debug and test a graph, use [static interrupts](../../concepts/human_in_the_loop.md#key-capabilities) (also known as static breakpoints) to step through the graph execution one node at a time or to pause the graph execution at specific nodes. Static interrupts are triggered at defined points either before or after a node executes. You can set static interrupts by specifying `interrupt_before` and `interrupt_after` at compile time or run time.
!!! warning
Static interrupts are **not** recommended for human-in-the-loop workflows. Use [dynamic interrupts](#pause-using-interrupt) instead.
=== "Compile time"
```python
# highlight-next-line
graph = graph_builder.compile( # (1)!
# highlight-next-line
interrupt_before=["node_a"], # (2)!
# highlight-next-line
interrupt_after=["node_b", "node_c"], # (3)!
checkpointer=checkpointer, # (4)!
)
config = {
"configurable": {
"thread_id": "some_thread"
}
}
# Run the graph until the breakpoint
graph.invoke(inputs, config=thread_config) # (5)!
# Resume the graph
graph.invoke(None, config=thread_config) # (6)!
```
1. The breakpoints are set during `compile` time.
2. `interrupt_before` specifies the nodes where execution should pause before the node is executed.
3. `interrupt_after` specifies the nodes where execution should pause after the node is executed.
4. A checkpointer is required to enable breakpoints.
5. The graph is run until the first breakpoint is hit.
6. The graph is resumed by passing in `None` for the input. This will run the graph until the next breakpoint is hit.
=== "Run time"
```python
# highlight-next-line
graph.invoke( # (1)!
inputs,
# highlight-next-line
interrupt_before=["node_a"], # (2)!
# highlight-next-line
interrupt_after=["node_b", "node_c"] # (3)!
config={
"configurable": {"thread_id": "some_thread"}
},
)
config = {
"configurable": {
"thread_id": "some_thread"
}
}
# Run the graph until the breakpoint
graph.invoke(inputs, config=config) # (4)!
# Resume the graph
graph.invoke(None, config=config) # (5)!
```
1. `graph.invoke` is called with the `interrupt_before` and `interrupt_after` parameters. This is a run-time configuration and can be changed for every invocation.
2. `interrupt_before` specifies the nodes where execution should pause before the node is executed.
3. `interrupt_after` specifies the nodes where execution should pause after the node is executed.
4. The graph is run until the first breakpoint is hit.
5. The graph is resumed by passing in `None` for the input. This will run the graph until the next breakpoint is hit.
!!! note
You cannot set static breakpoints at runtime for **sub-graphs**.
If you have a sub-graph, you must set the breakpoints at compilation time.
??? example "Setting static breakpoints"
```python
from IPython.display import Image, display
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
input: str
def step_1(state):
print("---Step 1---")
pass
def step_2(state):
print("---Step 2---")
pass
def step_3(state):
print("---Step 3---")
pass
builder = StateGraph(State)
builder.add_node("step_1", step_1)
builder.add_node("step_2", step_2)
builder.add_node("step_3", step_3)
builder.add_edge(START, "step_1")
builder.add_edge("step_1", "step_2")
builder.add_edge("step_2", "step_3")
builder.add_edge("step_3", END)
# Set up a checkpointer
checkpointer = InMemorySaver() # (1)!
graph = builder.compile(
checkpointer=checkpointer, # (2)!
interrupt_before=["step_3"] # (3)!
)
# View
display(Image(graph.get_graph().draw_mermaid_png()))
# Input
initial_input = {"input": "hello world"}
# Thread
thread = {"configurable": {"thread_id": "1"}}
# Run the graph until the first interruption
for event in graph.stream(initial_input, thread, stream_mode="values"):
print(event)
# This will run until the breakpoint
# You can get the state of the graph at this point
print(graph.get_state(config))
# You can continue the graph execution by passing in `None` for the input
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
```
### Use static interrupts in LangGraph Studio
You can use [LangGraph Studio](../../concepts/langgraph_studio.md) to debug your graph. You can set static breakpoints in the UI and then run the graph. You can also use the UI to inspect the graph state at any point in the execution.
![image](../../concepts/img/human_in_the_loop/static-interrupt.png){: style="max-height:400px"}
LangGraph Studio is free with [locally deployed applications](../../tutorials/langgraph-platform/local-server.md) using `langgraph dev`.
## Considerations
When using human-in-the-loop, there are some considerations to keep in mind.
@@ -938,7 +792,7 @@ def node_in_parent_graph(state: State):
from langgraph.graph import StateGraph
from langgraph.constants import START
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
class State(TypedDict):
@@ -964,7 +818,7 @@ def node_in_parent_graph(state: State):
print(f"Got an answer of {answer}")
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
subgraph_builder = StateGraph(State)
subgraph_builder.add_node("some_node", node_in_subgraph)
@@ -995,7 +849,7 @@ def node_in_parent_graph(state: State):
builder.add_edge(START, "parent_node")
# A checkpointer must be enabled for interrupts to work!
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {
@@ -1019,7 +873,7 @@ def node_in_parent_graph(state: State):
Entered `parent_node` a total of 1 times
Entered `node_in_subgraph` a total of 1 times
Entered human_node in sub-graph a total of 1 times
{'__interrupt__': (Interrupt(value='what is your name?', id='...'),)}
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['parent_node:4c3a0248-21f0-1287-eacf-3002bc304db4', 'human_node:2fe86d52-6f70-2a3f-6b2f-b1eededd6348'], when='during'),)}
--- Resuming ---
Entered `parent_node` a total of 2 times
Entered human_node in sub-graph a total of 2 times
@@ -1044,7 +898,7 @@ To avoid issues, refrain from dynamically changing the node's structure between
from langgraph.graph import StateGraph
from langgraph.constants import START
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
class State(TypedDict):
@@ -1078,7 +932,7 @@ To avoid issues, refrain from dynamically changing the node's structure between
builder.add_edge(START, "human_node")
# A checkpointer must be enabled for interrupts to work!
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {
@@ -1095,7 +949,8 @@ To avoid issues, refrain from dynamically changing the node's structure between
```
```pycon
{'__interrupt__': (Interrupt(value='what is your name?', id='...'),)}
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['human_node:3a007ef9-c30d-c357-1ec1-86a1a70d8fba'], when='during'),)}
Name: N/A. Age: John
{'human_node': {'age': 'John', 'name': 'N/A'}}
```
@@ -0,0 +1,342 @@
# Set breakpoints
There are two places where you can set breakpoints:
1. **Before** or **after** a node executes by setting breakpoints at **compile time** or **run time**. We call these [**static breakpoints**](#static-breakpoints).
2. **Inside** a node using the `NodeInterrupt` exception. We call these [**dynamic breakpoints**](#dynamic-breakpoints).
To use breakpoints, you will need to:
1. [**Specify a checkpointer**](../../concepts/persistence.md#checkpoints) to save the graph state after each step.
2. **Set breakpoints** to specify where execution should pause.
3. **Run the graph** with a [**thread ID**](../../concepts/persistence.md#threads) to pause execution at the breakpoint.
4. **Resume execution** using `invoke`/`ainvoke`/`stream`/`astream` passing a `None` as the argument for the inputs.
!!! tip
For a conceptual overview of breakpoints, see [Breakpoints](../../concepts/breakpoints.md).
## Static breakpoints
Static breakpoints are triggered either before or after a node executes. You can set static breakpoints by specifying `interrupt_before` and `interrupt_after` at compile time or run time.
Static breakpoints can be especially useful for debugging if you want to step through the graph execution one
node at a time or if you want to pause the graph execution at specific nodes.
=== "Compile time"
```python
# highlight-next-line
graph = graph_builder.compile( # (1)!
# highlight-next-line
interrupt_before=["node_a"], # (2)!
# highlight-next-line
interrupt_after=["node_b", "node_c"], # (3)!
checkpointer=checkpointer, # (4)!
)
config = {
"configurable": {
"thread_id": "some_thread"
}
}
# Run the graph until the breakpoint
graph.invoke(inputs, config=thread_config) # (5)!
# Resume the graph
graph.invoke(None, config=thread_config) # (6)!
```
1. The breakpoints are set during `compile` time.
2. `interrupt_before` specifies the nodes where execution should pause before the node is executed.
3. `interrupt_after` specifies the nodes where execution should pause after the node is executed.
4. A checkpointer is required to enable breakpoints.
5. The graph is run until the first breakpoint is hit.
6. The graph is resumed by passing in `None` for the input. This will run the graph until the next breakpoint is hit.
=== "Run time"
```python
# highlight-next-line
graph.invoke( # (1)!
inputs,
# highlight-next-line
interrupt_before=["node_a"], # (2)!
# highlight-next-line
interrupt_after=["node_b", "node_c"] # (3)!
config={
"configurable": {"thread_id": "some_thread"}
},
)
config = {
"configurable": {
"thread_id": "some_thread"
}
}
# Run the graph until the breakpoint
graph.invoke(inputs, config=config) # (4)!
# Resume the graph
graph.invoke(None, config=config) # (5)!
```
1. `graph.invoke` is called with the `interrupt_before` and `interrupt_after` parameters. This is a run-time configuration and can be changed for every invocation.
2. `interrupt_before` specifies the nodes where execution should pause before the node is executed.
3. `interrupt_after` specifies the nodes where execution should pause after the node is executed.
4. The graph is run until the first breakpoint is hit.
5. The graph is resumed by passing in `None` for the input. This will run the graph until the next breakpoint is hit.
!!! note
You cannot set static breakpoints at runtime for **sub-graphs**.
If you have a sub-graph, you must set the breakpoints at compilation time.
??? example "Setting static breakpoints"
```python
from IPython.display import Image, display
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
input: str
def step_1(state):
print("---Step 1---")
pass
def step_2(state):
print("---Step 2---")
pass
def step_3(state):
print("---Step 3---")
pass
builder = StateGraph(State)
builder.add_node("step_1", step_1)
builder.add_node("step_2", step_2)
builder.add_node("step_3", step_3)
builder.add_edge(START, "step_1")
builder.add_edge("step_1", "step_2")
builder.add_edge("step_2", "step_3")
builder.add_edge("step_3", END)
# Set up a checkpointer
checkpointer = InMemorySaver() # (1)!
graph = builder.compile(
checkpointer=checkpointer, # (2)!
interrupt_before=["step_3"] # (3)!
)
# View
display(Image(graph.get_graph().draw_mermaid_png()))
# Input
initial_input = {"input": "hello world"}
# Thread
thread = {"configurable": {"thread_id": "1"}}
# Run the graph until the first interruption
for event in graph.stream(initial_input, thread, stream_mode="values"):
print(event)
# This will run until the breakpoint
# You can get the state of the graph at this point
print(graph.get_state(config))
# You can continue the graph execution by passing in `None` for the input
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
```
## Dynamic breakpoints
Use dynamic breakpoints if you need to interrupt the graph from inside a given node based on a condition.
```python
from langgraph.errors import NodeInterrupt
def step_2(state: State) -> State:
# highlight-next-line
if len(state["input"]) > 5:
# highlight-next-line
raise NodeInterrupt( # (1)!
f"Received input that is longer than 5 characters: {state['foo']}"
)
return state
```
1. raise NodeInterrupt exception based on a some condition. In this example, we create a dynamic breakpoint if the length of the attribute `input` is longer than 5 characters.
<details class="example"><summary>Using dynamic breakpoints</summary>
```python
from typing_extensions import TypedDict
from IPython.display import Image, display
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.errors import NodeInterrupt
class State(TypedDict):
input: str
def step_1(state: State) -> State:
print("---Step 1---")
return state
def step_2(state: State) -> State:
# Let's optionally raise a NodeInterrupt
# if the length of the input is longer than 5 characters
if len(state["input"]) > 5:
raise NodeInterrupt(
f"Received input that is longer than 5 characters: {state['input']}"
)
print("---Step 2---")
return state
def step_3(state: State) -> State:
print("---Step 3---")
return state
builder = StateGraph(State)
builder.add_node("step_1", step_1)
builder.add_node("step_2", step_2)
builder.add_node("step_3", step_3)
builder.add_edge(START, "step_1")
builder.add_edge("step_1", "step_2")
builder.add_edge("step_2", "step_3")
builder.add_edge("step_3", END)
# Set up memory
memory = MemorySaver()
# Compile the graph with memory
graph = builder.compile(checkpointer=memory)
# View
display(Image(graph.get_graph().draw_mermaid_png()))
```
First, let's run the graph with an input that <= 5 characters long. This should safely ignore the interrupt condition we defined and return the original input at the end of the graph execution.
```python
initial_input = {"input": "hello"}
thread_config = {"configurable": {"thread_id": "1"}}
for event in graph.stream(initial_input, thread_config, stream_mode="values"):
print(event)
```
If we inspect the graph at this point, we can see that there are no more tasks left to run and that the graph indeed finished execution.
```python
state = graph.get_state(thread_config)
print(state.next)
print(state.tasks)
```
Now, let's run the graph with an input that's longer than 5 characters. This should trigger the dynamic interrupt we defined via raising a `NodeInterrupt` error inside the `step_2` node.
```python
initial_input = {"input": "hello world"}
thread_config = {"configurable": {"thread_id": "2"}}
# Run the graph until the first interruption
for event in graph.stream(initial_input, thread_config, stream_mode="values"):
print(event)
```
We can see that the graph now stopped while executing `step_2`. If we inspect the graph state at this point, we can see the information on what node is set to execute next (`step_2`), as well as what node raised the interrupt (also `step_2`), and additional information about the interrupt.
```python
state = graph.get_state(thread_config)
print(state.next)
print(state.tasks)
```
If we try to resume the graph from the breakpoint, we will simply interrupt again as our inputs & graph state haven't changed.
```python
# NOTE: to resume the graph from a dynamic interrupt we use the same syntax as with regular interrupts -- we pass None as the input
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
```
```python
state = graph.get_state(thread_config)
print(state.next)
print(state.tasks)
```
</details>
## Use with subgraphs
To add breakpoints to subgraph either:
* Define [static breakpoints](#static-breakpoints) by specifying them when **compiling** the subgraph.
* Define [dynamic breakpoints](#dynamic-breakpoints).
<details class="example"><summary>Add breakpoints to subgraphs</summary>
```python
from typing_extensions import TypedDict
from langgraph.graph import START, StateGraph
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt
class State(TypedDict):
foo: str
def subgraph_node_1(state: State):
return {"foo": state["foo"]}
subgraph_builder = StateGraph(State)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_edge(START, "subgraph_node_1")
subgraph = subgraph_builder.compile(interrupt_before=["subgraph_node_1"])
builder = StateGraph(State)
builder.add_node("node_1", subgraph) # directly include subgraph as a node
builder.add_edge(START, "node_1")
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"foo": ""}, config)
# Fetch state including subgraph state.
print(graph.get_state(config, subgraphs=True).tasks[0].state)
# resume the subgraph
graph.invoke(None, config)
```
</details>
@@ -4,7 +4,7 @@ To use [time-travel](../../concepts/time-travel.md) in LangGraph:
1. [Run the graph](#1-run-the-graph) with initial inputs using [`invoke`][langgraph.graph.state.CompiledStateGraph.invoke] or [`stream`][langgraph.graph.state.CompiledStateGraph.stream] methods.
2. [Identify a checkpoint in an existing thread](#2-identify-a-checkpoint): Use the [`get_state_history()`][langgraph.graph.state.CompiledStateGraph.get_state_history] method to retrieve the execution history for a specific `thread_id` and locate the desired `checkpoint_id`.
Alternatively, set an [interrupt](../../how-tos/human_in_the_loop/add-human-in-the-loop.md) before the node(s) where you want execution to pause. You can then find the most recent checkpoint recorded up to that interrupt.
Alternatively, set a [breakpoint](../../concepts/breakpoints.md) before the node(s) where you want execution to pause. You can then find the most recent checkpoint recorded up to that breakpoint.
3. [Update the graph state (optional)](#3-update-the-state-optional): Use the [`update_state`][langgraph.graph.state.CompiledStateGraph.update_state] method to modify the graph's state at the checkpoint and resume execution from alternative state.
4. [Resume execution from the checkpoint](#4-resume-execution-from-the-checkpoint): Use the `invoke` or `stream` methods with an input of `None` and a configuration containing the appropriate `thread_id` and `checkpoint_id`.
@@ -121,7 +121,7 @@
"\n",
"# highlight-next-line\n",
"from langgraph.types import Command, interrupt\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from IPython.display import Image, display\n",
"\n",
"\n",
@@ -157,7 +157,7 @@
"builder.add_edge(\"step_3\", END)\n",
"\n",
"# Set up memory\n",
"memory = InMemorySaver()\n",
"memory = MemorySaver()\n",
"\n",
"# Add\n",
"graph = builder.compile(checkpointer=memory)\n",
@@ -435,9 +435,9 @@
"workflow.add_edge(\"ask_human\", \"agent\")\n",
"\n",
"# Set up memory\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"memory = InMemorySaver()\n",
"memory = MemorySaver()\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
@@ -125,7 +125,7 @@
"memories = store.search((\"user_123\", \"memories\"), query=\"I like food?\", limit=5)\n",
"\n",
"for memory in memories:\n",
" print(f\"Memory: {memory.value['text']} (similarity: {memory.score})\")"
" print(f'Memory: {memory.value[\"text\"]} (similarity: {memory.score})')"
]
},
{
@@ -224,7 +224,7 @@
"from langgraph.prebuilt import create_react_agent\n",
"from langgraph.graph import add_messages\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.types import interrupt, Command\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
@@ -272,7 +272,7 @@
" return response[\"messages\"]\n",
"\n",
"\n",
"checkpointer = InMemorySaver()\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"def string_to_uuid(input_string):\n",
+2 -2
View File
@@ -375,7 +375,7 @@ def agent(state) -> Command[Literal["agent", "another_agent", "human"]]:
from langgraph.graph import MessagesState, StateGraph, START
from langgraph.prebuilt import create_react_agent, InjectedState
from langgraph.types import Command, interrupt
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
model = ChatAnthropic(model="claude-3-5-sonnet-latest")
@@ -467,7 +467,7 @@ def agent(state) -> Command[Literal["agent", "another_agent", "human"]]:
builder.add_edge(START, "travel_advisor")
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
```
@@ -28,9 +28,9 @@
"1. Create an instance of a checkpointer:\n",
"\n",
" ```python\n",
" from langgraph.checkpoint.memory import InMemorySaver\n",
" from langgraph.checkpoint.memory import MemorySaver\n",
" \n",
" checkpointer = InMemorySaver() \n",
" checkpointer = MemorySaver() \n",
" ```\n",
"\n",
"2. Pass `checkpointer` instance to the `entrypoint()` decorator:\n",
@@ -184,7 +184,7 @@
"from langchain_core.messages import BaseMessage\n",
"from langgraph.graph import add_messages\n",
"from langgraph.func import entrypoint, task\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"\n",
"@task\n",
@@ -193,7 +193,7 @@
" return response\n",
"\n",
"\n",
"checkpointer = InMemorySaver()\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"@entrypoint(checkpointer=checkpointer)\n",
@@ -261,7 +261,7 @@
"\n",
"To add thread-level persistence to our agent:\n",
"\n",
"1. Select a [checkpointer](../../concepts/persistence#checkpointer-libraries): here we will use [InMemorySaver](../../reference/checkpoints/#langgraph.checkpoint.memory.InMemorySaver), a simple in-memory checkpointer.\n",
"1. Select a [checkpointer](../../concepts/persistence#checkpointer-libraries): here we will use [MemorySaver](../../reference/checkpoints/#langgraph.checkpoint.memory.MemorySaver), a simple in-memory checkpointer.\n",
"2. Update our entrypoint to accept the previous messages state as a second argument. Here, we simply append the message updates to the previous sequence of messages.\n",
"3. Choose which values will be returned from the workflow and which will be saved by the checkpointer as `previous` using `entrypoint.final` (optional)"
]
@@ -272,10 +272,10 @@
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"# highlight-next-line\n",
"checkpointer = InMemorySaver()\n",
"checkpointer = MemorySaver()\n",
"\n",
"\n",
"# highlight-next-line\n",
+25 -25
View File
@@ -26,7 +26,7 @@ my_workflow.invoke({"value": 1, "another_value": 2})
```python
import uuid
from langgraph.func import entrypoint, task
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
# Task that checks if a number is even
@task
@@ -39,7 +39,7 @@ my_workflow.invoke({"value": 1, "another_value": 2})
return "The number is even." if is_even else "The number is odd."
# Create a checkpointer for persistence
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def workflow(inputs: dict) -> str:
@@ -63,7 +63,7 @@ my_workflow.invoke({"value": 1, "another_value": 2})
import uuid
from langchain.chat_models import init_chat_model
from langgraph.func import entrypoint, task
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
llm = init_chat_model('openai:gpt-3.5-turbo')
@@ -77,7 +77,7 @@ my_workflow.invoke({"value": 1, "another_value": 2})
]).content
# Create a checkpointer for persistence
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def workflow(topic: str) -> str:
@@ -114,7 +114,7 @@ def graph(numbers: list[int]) -> list[str]:
import uuid
from langchain.chat_models import init_chat_model
from langgraph.func import entrypoint, task
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
# Initialize the LLM model
llm = init_chat_model("openai:gpt-3.5-turbo")
@@ -129,7 +129,7 @@ def graph(numbers: list[int]) -> list[str]:
return response.content
# Create a checkpointer for persistence
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def workflow(topics: list[str]) -> str:
@@ -176,7 +176,7 @@ def some_workflow(some_input: dict) -> int:
import uuid
from typing import TypedDict
from langgraph.func import entrypoint
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph
# Define the shared state type
@@ -194,7 +194,7 @@ def some_workflow(some_input: dict) -> int:
graph = builder.compile()
# Define the functional API workflow
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def workflow(x: int) -> dict:
@@ -227,10 +227,10 @@ def my_workflow(inputs: dict) -> int:
```python
import uuid
from langgraph.func import entrypoint
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
# Initialize a checkpointer
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
# A reusable sub-workflow that multiplies a number
@entrypoint()
@@ -258,10 +258,10 @@ Example of using the streaming API to stream both updates and custom data.
```python
from langgraph.func import entrypoint
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.config import get_stream_writer # (1)!
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def main(inputs: dict) -> int:
@@ -316,7 +316,7 @@ for mode, chunk in main.stream( # (5)!
## Retry policy
```python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import RetryPolicy
@@ -337,7 +337,7 @@ def get_info():
raise ValueError('Failure')
return "OK"
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def main(inputs, writer):
@@ -392,7 +392,7 @@ for chunk in main.stream({"x": 5}, stream_mode="updates"):
```python
import time
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import StreamWriter
@@ -414,7 +414,7 @@ def get_info():
return "OK"
# Initialize an in-memory checkpointer for persistence
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
@task
def slow_task():
@@ -504,9 +504,9 @@ def step_3(input_query):
We can now compose these tasks in an [entrypoint](../concepts/functional_api.md#entrypoint):
```python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
@@ -577,12 +577,12 @@ def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]:
We can now update our [entrypoint](../concepts/functional_api.md#entrypoint) to review the generated tool calls. If a tool call is accepted or revised, we execute in the same way as before. Otherwise, we just append the `ToolMessage` supplied by the human. The results of prior tasks — in this case the initial model call — are persisted, so that they are not run again following the `interrupt`.
```python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph.message import add_messages
from langgraph.types import Command, interrupt
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
@@ -757,9 +757,9 @@ Use `entrypoint.final` to decouple what is returned to the caller from what is p
```python
from typing import Optional
from langgraph.func import entrypoint
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def accumulate(n: int, *, previous: Optional[int]) -> entrypoint.final[int, int]:
@@ -777,14 +777,14 @@ print(accumulate.invoke(3, config=config)) # 3
### Chatbot example
An example of a simple chatbot using the functional API and the `InMemorySaver` checkpointer.
An example of a simple chatbot using the functional API and the `MemorySaver` checkpointer.
The bot is able to remember the previous conversation and continue from where it left off.
```python
from langchain_core.messages import BaseMessage
from langgraph.graph import add_messages
from langgraph.func import entrypoint, task
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-3-5-sonnet-latest")
@@ -794,7 +794,7 @@ def call_model(messages: list[BaseMessage]):
response = model.invoke(messages)
return response
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
@entrypoint(checkpointer=checkpointer)
def workflow(inputs: list[BaseMessage], *, previous: list[BaseMessage]):
+1 -1
View File
@@ -28,4 +28,4 @@ title: LangGraph
}
</style>
{% include-markdown "../../README.md" %}
{!../README.md!}
+1 -2
View File
@@ -2,6 +2,5 @@
options:
members:
- TAG_HIDDEN
- TAG_NOSTREAM
- START
- END
- END
-18
View File
@@ -1,18 +0,0 @@
# Runtime
::: langgraph.runtime.Runtime
options:
show_root_heading: true
show_root_full_path: false
members:
- context
- store
- stream_writer
- previous
::: langgraph.runtime
options:
members:
- get_runtime
-87
View File
@@ -1,87 +0,0 @@
=== "OpenAI"
```shell
pip install -U "langchain[openai]"
```
```python
import os
from langchain.chat_models import init_chat_model
os.environ["OPENAI_API_KEY"] = "sk-..."
llm = init_chat_model("openai:gpt-4.1")
```
👉 Read the [OpenAI integration docs](https://python.langchain.com/docs/integrations/chat/openai/)
=== "Anthropic"
```shell
pip install -U "langchain[anthropic]"
```
```python
import os
from langchain.chat_models import init_chat_model
os.environ["ANTHROPIC_API_KEY"] = "sk-..."
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
👉 Read the [Anthropic integration docs](https://python.langchain.com/docs/integrations/chat/anthropic/)
=== "Azure"
```shell
pip install -U "langchain[openai]"
```
```python
import os
from langchain.chat_models import init_chat_model
os.environ["AZURE_OPENAI_API_KEY"] = "..."
os.environ["AZURE_OPENAI_ENDPOINT"] = "..."
os.environ["OPENAI_API_VERSION"] = "2025-03-01-preview"
llm = init_chat_model(
"azure_openai:gpt-4.1",
azure_deployment=os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"],
)
```
👉 Read the [Azure integration docs](https://python.langchain.com/docs/integrations/chat/azure_chat_openai/)
=== "Google Gemini"
```shell
pip install -U "langchain[google-genai]"
```
```python
import os
from langchain.chat_models import init_chat_model
os.environ["GOOGLE_API_KEY"] = "..."
llm = init_chat_model("google_genai:gemini-2.0-flash")
```
👉 Read the [Google GenAI integration docs](https://python.langchain.com/docs/integrations/chat/google_generative_ai/)
=== "AWS Bedrock"
```shell
pip install -U "langchain[aws]"
```
```python
from langchain.chat_models import init_chat_model
# Follow the steps here to configure your credentials:
# https://docs.aws.amazon.com/bedrock/latest/userguide/getting-started.html
llm = init_chat_model(
"anthropic.claude-3-5-sonnet-20240620-v1:0",
model_provider="bedrock_converse",
)
```
👉 Read the [AWS Bedrock integration docs](https://python.langchain.com/docs/integrations/chat/bedrock/)
@@ -256,7 +256,7 @@
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from typing import Annotated\n",
@@ -267,7 +267,7 @@
" messages: Annotated[list, add_messages]\n",
"\n",
"\n",
"memory = InMemorySaver()\n",
"memory = MemorySaver()\n",
"workflow = StateGraph(State)\n",
"workflow.add_node(\"info\", info_chain)\n",
"workflow.add_node(\"prompt\", prompt_gen_chain)\n",
@@ -1124,7 +1124,7 @@
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import END, StateGraph, START\n",
"from langgraph.prebuilt import tools_condition\n",
"\n",
@@ -1144,7 +1144,7 @@
"\n",
"# The checkpointer lets the graph persist its state\n",
"# this is a complete memory for the entire graph.\n",
"memory = InMemorySaver()\n",
"memory = MemorySaver()\n",
"part_1_graph = builder.compile(checkpointer=memory)"
]
},
@@ -1943,7 +1943,7 @@
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.prebuilt import tools_condition\n",
"\n",
@@ -1967,7 +1967,7 @@
")\n",
"builder.add_edge(\"tools\", \"assistant\")\n",
"\n",
"memory = InMemorySaver()\n",
"memory = MemorySaver()\n",
"part_2_graph = builder.compile(\n",
" checkpointer=memory,\n",
" # NEW: The graph will always halt before executing the \"tools\" node.\n",
@@ -2532,7 +2532,7 @@
"source": [
"from typing import Literal\n",
"\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.prebuilt import tools_condition\n",
"\n",
@@ -2576,7 +2576,7 @@
"builder.add_edge(\"safe_tools\", \"assistant\")\n",
"builder.add_edge(\"sensitive_tools\", \"assistant\")\n",
"\n",
"memory = InMemorySaver()\n",
"memory = MemorySaver()\n",
"part_3_graph = builder.compile(\n",
" checkpointer=memory,\n",
" # NEW: The graph will always halt before executing the \"tools\" node.\n",
@@ -3477,7 +3477,7 @@
"source": [
"from typing import Literal\n",
"\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.prebuilt import tools_condition\n",
"\n",
@@ -3841,7 +3841,7 @@
"builder.add_conditional_edges(\"fetch_user_info\", route_to_workflow)\n",
"\n",
"# Compile graph\n",
"memory = InMemorySaver()\n",
"memory = MemorySaver()\n",
"part_4_graph = builder.compile(\n",
" checkpointer=memory,\n",
" # Let the user approve or deny the use of sensitive tools\n",
@@ -63,7 +63,7 @@ Next, add a "`chatbot`" node. **Nodes** represent units of work and are typicall
Let's first select a chat model:
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
<!---
```python
@@ -73,7 +73,7 @@ For the `StateGraph` you created in the [first tutorial](./1-build-basic-chatbot
Let's first select our LLM:
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
<!---
```python
@@ -286,7 +286,7 @@ For ease of use, adjust your code to replace the following with LangGraph prebui
- `BasicToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode)
- `route_tools` is replaced with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition)
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
```python hl_lines="25 30"
@@ -10,14 +10,14 @@ We will see later that **checkpointing** is _much_ more powerful than simple cha
This tutorial builds on [Add tools](./2-add-tools.md).
## 1. Create a `InMemorySaver` checkpointer
## 1. Create a `MemorySaver` checkpointer
Create a `InMemorySaver` checkpointer:
Create a `MemorySaver` checkpointer:
``` python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
memory = InMemorySaver()
memory = MemorySaver()
```
This is in-memory checkpointer, which is convenient for the tutorial. However, in a production application, you would likely change this to use `SqliteSaver` or `PostgresSaver` and connect a database.
@@ -154,7 +154,7 @@ The snapshot above contains the current state values, corresponding config, and
Check out the code snippet below to review the graph from this tutorial:
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
<!---
```python
@@ -172,7 +172,7 @@ from langchain_tavily import TavilySearch
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
@@ -200,7 +200,7 @@ graph_builder.add_conditional_edges(
)
graph_builder.add_edge("tools", "chatbot")
graph_builder.set_entry_point("chatbot")
memory = InMemorySaver()
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
@@ -14,7 +14,7 @@ Starting with the existing code from the [Add memory to the chatbot](./3-add-mem
Let's first select a chat model:
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
<!---
```python
@@ -33,7 +33,7 @@ from langchain_tavily import TavilySearch
from langchain_core.tools import tool
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
@@ -85,7 +85,7 @@ graph_builder.add_edge(START, "chatbot")
We compile the graph with a checkpointer, as before:
```python
memory = InMemorySaver()
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
@@ -221,7 +221,7 @@ The input has been received and processed as a tool message. Review this call's
Check out the code snippet below to review the graph from this tutorial:
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
```python
from typing import Annotated
@@ -230,7 +230,7 @@ from langchain_tavily import TavilySearch
from langchain_core.tools import tool
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
@@ -268,7 +268,7 @@ graph_builder.add_conditional_edges(
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
memory = InMemorySaver()
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
@@ -221,7 +221,7 @@ Manual state updates will [generate a trace](https://smith.langchain.com/public/
Check out the code snippet below to review the graph from this tutorial:
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
<!---
```python
@@ -239,7 +239,7 @@ from langchain_core.messages import ToolMessage
from langchain_core.tools import InjectedToolCallId, tool
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
@@ -301,7 +301,7 @@ graph_builder.add_conditional_edges(
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
memory = InMemorySaver()
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
@@ -14,7 +14,7 @@ You can create these types of experiences using LangGraph's built-in **time trav
Rewind your graph by fetching a checkpoint using the graph's `get_state_history` method. You can then resume execution at this previous point in time.
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
{!snippets/chat_model_tabs.md!}
<!---
```python
@@ -31,7 +31,7 @@ from langchain_tavily import TavilySearch
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
@@ -60,7 +60,7 @@ graph_builder.add_conditional_edges(
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
memory = InMemorySaver()
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
@@ -12,9 +12,9 @@ Before you begin, ensure you have the following:
=== "Python server"
Python >= 3.11 is required.
```shell
# Python >= 3.11 is required.
pip install --upgrade "langgraph-cli[inmem]"
```
@@ -540,11 +540,11 @@
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m System Message \u001b[0m================================\n",
"================================\u001B[1m System Message \u001B[0m================================\n",
"\n",
"Given a user query, create a plan to solve it with the utmost parallelizability. Each plan should comprise an action from the following \u001b[33;1m\u001b[1;3m{num_tools}\u001b[0m types:\n",
"\u001b[33;1m\u001b[1;3m{tool_descriptions}\u001b[0m\n",
"\u001b[33;1m\u001b[1;3m{num_tools}\u001b[0m. join(): Collects and combines results from prior actions.\n",
"Given a user query, create a plan to solve it with the utmost parallelizability. Each plan should comprise an action from the following \u001B[33;1m\u001B[1;3m{num_tools}\u001B[0m types:\n",
"\u001B[33;1m\u001B[1;3m{tool_descriptions}\u001B[0m\n",
"\u001B[33;1m\u001B[1;3m{num_tools}\u001B[0m. join(): Collects and combines results from prior actions.\n",
"\n",
" - An LLM agent is called upon invoking join() to either finalize the user query or wait until the plans are executed.\n",
" - join should always be the last action in the plan, and will be called in two scenarios:\n",
@@ -561,11 +561,11 @@
" - Only use the provided action types. If a query cannot be addressed using these, invoke the join action for the next steps.\n",
" - Never introduce new actions other than the ones provided.\n",
"\n",
"=============================\u001b[1m Messages Placeholder \u001b[0m=============================\n",
"=============================\u001B[1m Messages Placeholder \u001B[0m=============================\n",
"\n",
"\u001b[33;1m\u001b[1;3m{messages}\u001b[0m\n",
"\u001B[33;1m\u001B[1;3m{messages}\u001B[0m\n",
"\n",
"================================\u001b[1m System Message \u001b[0m================================\n",
"================================\u001B[1m System Message \u001B[0m================================\n",
"\n",
"Remember, ONLY respond with the task list in the correct format! E.g.:\n",
"idx. tool(arg_name=args)\n",
@@ -605,7 +605,7 @@
" llm: BaseChatModel, tools: Sequence[BaseTool], base_prompt: ChatPromptTemplate\n",
"):\n",
" tool_descriptions = \"\\n\".join(\n",
" f\"{i + 1}. {tool.description}\\n\"\n",
" f\"{i+1}. {tool.description}\\n\"\n",
" for i, tool in enumerate(\n",
" tools\n",
" ) # +1 to offset the 0 starting index, we want it count normally from 1.\n",
@@ -378,7 +378,7 @@
"\n",
"async def execute_step(state: PlanExecute):\n",
" plan = state[\"plan\"]\n",
" plan_str = \"\\n\".join(f\"{i + 1}. {step}\" for i, step in enumerate(plan))\n",
" plan_str = \"\\n\".join(f\"{i+1}. {step}\" for i, step in enumerate(plan))\n",
" task = plan[0]\n",
" task_formatted = f\"\"\"For the following plan:\n",
"{plan_str}\\n\\nYou are tasked with executing step {1}, {task}.\"\"\"\n",
@@ -322,7 +322,7 @@
"from typing import Annotated, List, Sequence\n",
"from langgraph.graph import END, StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
@@ -361,7 +361,7 @@
"\n",
"builder.add_conditional_edges(\"generate\", should_continue)\n",
"builder.add_edge(\"reflect\", \"generate\")\n",
"memory = InMemorySaver()\n",
"memory = MemorySaver()\n",
"graph = builder.compile(checkpointer=memory)"
]
},
+7 -7
View File
@@ -302,7 +302,7 @@
"def format_docs(docs: List[Doc]) -> str:\n",
" xml_table = \"<conversations>\\n\"\n",
" for doc in docs:\n",
" xml_table += f\"<conv_summ id={doc['id']}>{doc['summary']}</conv_summ>\\n\"\n",
" xml_table += f'<conv_summ id={doc[\"id\"]}>{doc[\"summary\"]}</conv_summ>\\n'\n",
" xml_table += \"</conversations>\"\n",
" return xml_table\n",
"\n",
@@ -311,9 +311,9 @@
" xml = \"<cluster_table>\\n\"\n",
" for label in clusters:\n",
" xml += \" <cluster>\\n\"\n",
" xml += f\" <id>{label['id']}</id>\\n\"\n",
" xml += f\" <name>{label['name']}</name>\\n\"\n",
" xml += f\" <description>{label['description']}</description>\\n\"\n",
" xml += f' <id>{label[\"id\"]}</id>\\n'\n",
" xml += f' <name>{label[\"name\"]}</name>\\n'\n",
" xml += f' <description>{label[\"description\"]}</description>\\n'\n",
" xml += \" </cluster>\\n\"\n",
" xml += \"</cluster_table>\"\n",
" return xml\n",
@@ -600,13 +600,13 @@
" turns.append(\n",
" f\"\"\"\n",
"<human idx={idx}>\n",
"{run.inputs[\"question\"]}\n",
"{run.inputs['question']}\n",
"</human>\"\"\"\n",
" )\n",
" if run.outputs and run.outputs[\"output\"]:\n",
" turns.append(\n",
" f\"\"\"<ai idx={idx + 1}>\n",
"{run.outputs[\"output\"]}\n",
" f\"\"\"<ai idx={idx+1}>\n",
"{run.outputs['output']}\n",
"</ai>\"\"\"\n",
" )\n",
" return {\n",
+35 -37
View File
@@ -272,7 +272,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
@@ -280,10 +280,10 @@
"from typing import Optional, Dict, Any\n",
"from typing_extensions import Annotated, TypedDict\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.runtime import Runtime\n",
"\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.types import Send\n",
"from langchain_core.runnables import RunnableConfig\n",
"from langgraph.constants import Send\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"\n",
"def update_candidates(\n",
@@ -307,27 +307,22 @@
" depth: Annotated[int, operator.add]\n",
"\n",
"\n",
"class Context(TypedDict, total=False):\n",
"class Configuration(TypedDict, total=False):\n",
" max_depth: int\n",
" threshold: float\n",
" k: int\n",
" beam_size: int\n",
"\n",
"\n",
"class EnsuredContext(TypedDict):\n",
" max_depth: int\n",
" threshold: float\n",
" k: int\n",
" beam_size: int\n",
"\n",
"\n",
"def _ensure_context(ctx: Context) -> EnsuredContext:\n",
"def _ensure_configurable(config: RunnableConfig) -> Configuration:\n",
" \"\"\"Get params that configure the search algorithm.\"\"\"\n",
" configurable = config.get(\"configurable\", {})\n",
" return {\n",
" \"max_depth\": ctx.get(\"max_depth\", 10),\n",
" \"threshold\": ctx.get(\"threshold\", 0.9),\n",
" \"k\": ctx.get(\"k\", 5),\n",
" \"beam_size\": ctx.get(\"beam_size\", 3),\n",
" **configurable,\n",
" \"max_depth\": configurable.get(\"max_depth\", 10),\n",
" \"threshold\": config.get(\"threshold\", 0.9),\n",
" \"k\": configurable.get(\"k\", 5),\n",
" \"beam_size\": configurable.get(\"beam_size\", 3),\n",
" }\n",
"\n",
"\n",
@@ -335,11 +330,9 @@
" seed: Optional[Candidate]\n",
"\n",
"\n",
"def expand(\n",
" state: ExpansionState, *, runtime: Runtime[Context]\n",
") -> Dict[str, List[Candidate]]:\n",
"def expand(state: ExpansionState, *, config: RunnableConfig) -> Dict[str, List[str]]:\n",
" \"\"\"Generate the next state.\"\"\"\n",
" ctx = _ensure_context(runtime.context)\n",
" configurable = _ensure_configurable(config)\n",
" if not state.get(\"seed\"):\n",
" candidate_str = \"\"\n",
" else:\n",
@@ -349,8 +342,9 @@
" {\n",
" \"problem\": state[\"problem\"],\n",
" \"candidate\": candidate_str,\n",
" \"k\": ctx[\"k\"],\n",
" \"k\": configurable[\"k\"],\n",
" },\n",
" config=config,\n",
" )\n",
" except Exception:\n",
" return {\"candidates\": []}\n",
@@ -360,7 +354,7 @@
" return {\"candidates\": new_candidates}\n",
"\n",
"\n",
"def score(state: ToTState) -> Dict[str, Any]:\n",
"def score(state: ToTState) -> Dict[str, List[float]]:\n",
" \"\"\"Evaluate the candidate generations.\"\"\"\n",
" candidates = state[\"candidates\"]\n",
" scored = []\n",
@@ -369,9 +363,11 @@
" return {\"scored_candidates\": scored, \"candidates\": \"clear\"}\n",
"\n",
"\n",
"def prune(state: ToTState, *, runtime: Runtime[Context]) -> Dict[str, Any]:\n",
"def prune(\n",
" state: ToTState, *, config: RunnableConfig\n",
") -> Dict[str, List[Dict[str, Any]]]:\n",
" scored_candidates = state[\"scored_candidates\"]\n",
" beam_size = _ensure_context(runtime.context)[\"beam_size\"]\n",
" beam_size = _ensure_configurable(config)[\"beam_size\"]\n",
" organized = sorted(\n",
" scored_candidates, key=lambda candidate: candidate[1], reverse=True\n",
" )\n",
@@ -387,11 +383,11 @@
"\n",
"\n",
"def should_terminate(\n",
" state: ToTState, runtime: Runtime[Context]\n",
" state: ToTState, config: RunnableConfig\n",
") -> Union[Literal[\"__end__\"], Send]:\n",
" ctx = _ensure_context(runtime.context)\n",
" solved = state[\"candidates\"][0].score >= ctx[\"threshold\"]\n",
" if solved or state[\"depth\"] >= ctx[\"max_depth\"]:\n",
" configurable = _ensure_configurable(config)\n",
" solved = state[\"candidates\"][0].score >= configurable[\"threshold\"]\n",
" if solved or state[\"depth\"] >= configurable[\"max_depth\"]:\n",
" return \"__end__\"\n",
" return [\n",
" Send(\"expand\", {**state, \"somevalseed\": candidate})\n",
@@ -400,7 +396,7 @@
"\n",
"\n",
"# Create the graph\n",
"builder = StateGraph(state_schema=ToTState, context_schema=Context)\n",
"builder = StateGraph(state_schema=ToTState, config_schema=Configuration)\n",
"\n",
"# Add nodes\n",
"builder.add_node(expand)\n",
@@ -416,7 +412,7 @@
"builder.add_edge(\"__start__\", \"expand\")\n",
"\n",
"# Compile the graph\n",
"graph = builder.compile(checkpointer=InMemorySaver())"
"graph = builder.compile(checkpointer=MemorySaver())"
]
},
{
@@ -471,11 +467,13 @@
}
],
"source": [
"for step in graph.stream(\n",
" {\"problem\": puzzles[42]},\n",
" config={\"configurable\": {\"thread_id\": \"test_1\"}},\n",
" context={\"depth\": 10},\n",
"):\n",
"config = {\n",
" \"configurable\": {\n",
" \"thread_id\": \"test_1\",\n",
" \"depth\": 10,\n",
" }\n",
"}\n",
"for step in graph.stream({\"problem\": puzzles[42]}, config):\n",
" print(step)"
]
},
@@ -493,7 +491,7 @@
}
],
"source": [
"final_state = graph.get_state({\"configurable\": {\"thread_id\": \"test_1\"}})\n",
"final_state = graph.get_state(config)\n",
"winning_solution = final_state.values[\"candidates\"][0]\n",
"search_depth = final_state.values[\"depth\"]\n",
"if winning_solution[1] == 1:\n",
+4 -4
View File
@@ -1029,7 +1029,7 @@
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import END, StateGraph, START\n",
"\n",
"builder = StateGraph(State)\n",
@@ -1053,7 +1053,7 @@
"builder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n",
"\n",
"\n",
"checkpointer = InMemorySaver()\n",
"checkpointer = MemorySaver()\n",
"graph = builder.compile(checkpointer=checkpointer)"
]
},
@@ -1327,7 +1327,7 @@
"outputs": [],
"source": [
"# This is all the same as before\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import END, StateGraph, START\n",
"\n",
"builder = StateGraph(State)\n",
@@ -1353,7 +1353,7 @@
"\n",
"\n",
"builder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n",
"checkpointer = InMemorySaver()"
"checkpointer = MemorySaver()"
]
},
{
+12 -10
View File
@@ -54,7 +54,6 @@ plugins:
separator: '[\s\u200b\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;'
- autorefs
- tags
- include-markdown
- mkdocstrings:
custom_templates: templates
handlers:
@@ -103,15 +102,14 @@ nav:
- 5. Customize state: tutorials/get-started/5-customize-state.md
- 6. Time travel: tutorials/get-started/6-time-travel.md
- Run a local server: tutorials/langgraph-platform/local-server.md
- General concepts:
- Agent development:
- Workflows & agents: tutorials/workflows.md
- Prebuilt components: agents/overview.md
- Run an agent: agents/run_agents.md
- Agent architectures: concepts/agentic_concepts.md
- Guides:
- guides/index.md
- Agent development:
- Overview: agents/overview.md
- Run an agent: agents/run_agents.md
- LangGraph APIs:
- Graph API:
- Overview: concepts/low_level.md
@@ -143,6 +141,10 @@ nav:
- Overview: concepts/human_in_the_loop.md
- Add human intervention: how-tos/human_in_the_loop/add-human-in-the-loop.md
- Use Server API: cloud/how-tos/add-human-in-the-loop.md
- Breakpoints:
- Overview: concepts/breakpoints.md
- Set breakpoints: how-tos/human_in_the_loop/breakpoints.md
- Use Server API: cloud/how-tos/human_in_the_loop_breakpoint.md
- Time travel:
- Overview: concepts/time-travel.md
- Use time travel: how-tos/human_in_the_loop/time-travel.md
@@ -158,10 +160,8 @@ nav:
- Overview: concepts/mcp.md
- Use MCP: agents/mcp.md
- Server API: concepts/server-mcp.md
- Tracing:
- Overview: concepts/tracing.md
- Enable tracing: how-tos/enable-tracing.md
- Evaluate performance: agents/evals.md
- Evaluation:
- Basic implementation: agents/evals.md
- Platform-only capabilities:
- LangGraph Platform:
- Overview: concepts/langgraph_platform.md
@@ -250,7 +250,6 @@ nav:
- Storage: reference/store.md
- Caching: reference/cache.md
- Types: reference/types.md
- Runtime: reference/runtime.md
- Config: reference/config.md
- Errors: reference/errors.md
- Constants: reference/constants.md
@@ -358,9 +357,12 @@ markdown_extensions:
combine_header_slug: true
- pymdownx.tasklist:
custom_checkbox: true
- markdown_include.include:
base_path: ./
- github-callouts
hooks:
- _scripts/notebook_hooks.py
- _scripts/copy_page_hooks.py
extra:
social:
- icon: fontawesome/brands/js
+2 -2
View File
@@ -37,7 +37,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
const data = JSON.parse(rawContent);
const content = `Source: ${window.location.href}\n\n${data.markdown}`;
const content = `# ${data.title}\n\nSource: ${window.location.href}\n\n${data.markdown}`;
navigator.clipboard.writeText(content).then(() => {
// Simple notification
@@ -101,7 +101,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
};
option2.onmouseout = function() { this.style.background = 'transparent'; };
option2.onclick = function() {
window.open('/langgraph/llms-txt-overview/', '_blank');
window.open('/llms-txt-overview/', '_blank');
dropdown.style.display = 'none';
};
+2 -5
View File
@@ -7,7 +7,7 @@ name = "langgraph-docs"
version = "0.0.1"
description = "LangGraph docs"
authors = []
requires-python = "~=3.11"
requires-python = "~=3.10"
readme = "README.md"
license = "MIT"
dependencies = [
@@ -48,7 +48,6 @@ docs = [
"ruff",
"jupyter",
"langchain-cohere",
"mkdocs-include-markdown-plugin>=7.1.6",
]
test = [
"langchain",
@@ -112,6 +111,4 @@ extend-include = ["*.ipynb"]
[tool.codespell]
# https://mypy.readthedocs.io/en/stable/config_file.html
# comma-separated list
ignore-words-list = "infor,thead,stdio,nd,jupyter,lets,lite,uis,deque"
# Exclude generated files and directories
skip = "*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.css.map,*.js.map"
ignore-words-list = "infor"
Generated
+2091 -1469
View File
File diff suppressed because it is too large Load Diff
@@ -154,7 +154,7 @@
"id": "2dff2209-44c7-4e2c-b607-ba6675f9e45f",
"metadata": {},
"outputs": [],
"source": ["from langgraph.checkpoint.memory import InMemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(GraphState)\n\n# Define the nodes\nbuilder.add_node(\"generate\", generate) # generation solution\nbuilder.add_node(\"check_code\", code_check) # check code\n\n# Build graph\nbuilder.add_edge(START, \"generate\")\nbuilder.add_edge(\"generate\", \"check_code\")\nbuilder.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"generate\": \"generate\",\n },\n)\n\nmemory = InMemorySaver()\ngraph = builder.compile(checkpointer=memory)"]
"source": ["from langgraph.checkpoint.memory import MemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(GraphState)\n\n# Define the nodes\nbuilder.add_node(\"generate\", generate) # generation solution\nbuilder.add_node(\"check_code\", code_check) # check code\n\n# Build graph\nbuilder.add_edge(START, \"generate\")\nbuilder.add_edge(\"generate\", \"check_code\")\nbuilder.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"generate\": \"generate\",\n },\n)\n\nmemory = MemorySaver()\ngraph = builder.compile(checkpointer=memory)"]
},
{
"cell_type": "code",
@@ -284,9 +284,11 @@ class PostgresSaver(BasePostgresSaver):
configurable = config["configurable"].copy()
thread_id = configurable.pop("thread_id")
checkpoint_ns = configurable.pop("checkpoint_ns")
checkpoint_id = configurable.pop("checkpoint_id", None)
checkpoint_id = configurable.pop(
"checkpoint_id", configurable.pop("thread_ts", None)
)
copy = checkpoint.copy()
copy["channel_values"] = copy["channel_values"].copy()
next_config = {
"configurable": {
"thread_id": thread_id,
@@ -295,28 +297,16 @@ class PostgresSaver(BasePostgresSaver):
}
}
# inline primitive values in checkpoint table
# others are stored in blobs table
blob_values = {}
for k, v in checkpoint["channel_values"].items():
if v is None or isinstance(v, (str, int, float, bool)):
pass
else:
blob_values[k] = copy["channel_values"].pop(k)
with self._cursor(pipeline=True) as cur:
if blob_versions := {
k: v for k, v in new_versions.items() if k in blob_values
}:
cur.executemany(
self.UPSERT_CHECKPOINT_BLOBS_SQL,
self._dump_blobs(
thread_id,
checkpoint_ns,
blob_values,
blob_versions,
),
)
cur.executemany(
self.UPSERT_CHECKPOINT_BLOBS_SQL,
self._dump_blobs(
thread_id,
checkpoint_ns,
copy.pop("channel_values"), # type: ignore[misc]
new_versions,
),
)
cur.execute(
self.UPSERT_CHECKPOINTS_SQL,
(
@@ -449,10 +439,7 @@ class PostgresSaver(BasePostgresSaver):
},
{
**value["checkpoint"],
"channel_values": {
**value["checkpoint"].get("channel_values"),
**self._load_blobs(value["channel_values"]),
},
"channel_values": self._load_blobs(value["channel_values"]),
},
value["metadata"],
(
@@ -240,10 +240,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
configurable = config["configurable"].copy()
thread_id = configurable.pop("thread_id")
checkpoint_ns = configurable.pop("checkpoint_ns")
checkpoint_id = configurable.pop("checkpoint_id", None)
checkpoint_id = configurable.pop(
"checkpoint_id", configurable.pop("thread_ts", None)
)
copy = checkpoint.copy()
copy["channel_values"] = copy["channel_values"].copy()
next_config = {
"configurable": {
"thread_id": thread_id,
@@ -252,29 +253,17 @@ class AsyncPostgresSaver(BasePostgresSaver):
}
}
# inline primitive values in checkpoint table
# others are stored in blobs table
blob_values = {}
for k, v in checkpoint["channel_values"].items():
if v is None or isinstance(v, (str, int, float, bool)):
pass
else:
blob_values[k] = copy["channel_values"].pop(k)
async with self._cursor(pipeline=True) as cur:
if blob_versions := {
k: v for k, v in new_versions.items() if k in blob_values
}:
await cur.executemany(
self.UPSERT_CHECKPOINT_BLOBS_SQL,
await asyncio.to_thread(
self._dump_blobs,
thread_id,
checkpoint_ns,
blob_values,
blob_versions,
),
)
await cur.executemany(
self.UPSERT_CHECKPOINT_BLOBS_SQL,
await asyncio.to_thread(
self._dump_blobs,
thread_id,
checkpoint_ns,
copy.pop("channel_values"), # type: ignore[misc]
new_versions,
),
)
await cur.execute(
self.UPSERT_CHECKPOINTS_SQL,
(
@@ -408,10 +397,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
},
{
**value["checkpoint"],
"channel_values": {
**value["checkpoint"].get("channel_values"),
**self._load_blobs(value["channel_values"]),
},
"channel_values": self._load_blobs(value["channel_values"]),
},
value["metadata"],
(
@@ -191,7 +191,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
) -> None:
warnings.warn(
"ShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
"Use PostgresSaver instead, and invoke the graph with `graph.invoke(..., durability='exit')`.",
"Use PostgresSaver instead, and invoke the graph with `graph.invoke(..., checkpoint_during=False)`.",
DeprecationWarning,
stacklevel=2,
)
@@ -547,7 +547,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
) -> None:
warnings.warn(
"AsyncShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
"Use AsyncPostgresSaver instead, and invoke the graph with `await graph.ainvoke(..., durability='exit')`.",
"Use AsyncPostgresSaver instead, and invoke the graph with `await graph.ainvoke(..., checkpoint_during=False)`.",
DeprecationWarning,
stacklevel=2,
)
@@ -756,7 +756,6 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
If provided, will create a connection pool and use it instead of a single connection.
This overrides the `pipeline` argument.
index: The index configuration for the store.
ttl: The TTL configuration for the store.
Returns:
PostgresStore: A new PostgresStore instance.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint-postgres"
version = "2.0.23"
version = "2.0.22"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
requires-python = ">=3.9"
+2 -1
View File
@@ -161,7 +161,8 @@ def test_data():
config_1: RunnableConfig = {
"configurable": {
"thread_id": "thread-1",
"checkpoint_id": "1",
# for backwards compatibility testing
"thread_ts": "1",
"checkpoint_ns": "",
}
}
+2 -1
View File
@@ -143,7 +143,8 @@ def test_data():
config_1: RunnableConfig = {
"configurable": {
"thread_id": "thread-1",
"checkpoint_id": "1",
# for backwards compatibility testing
"thread_ts": "1",
"checkpoint_ns": "",
}
}
+27 -27
View File
@@ -28,11 +28,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2025.7.9"
version = "2025.6.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" }
sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" },
{ url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
]
[[package]]
@@ -304,7 +304,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "2.1.1"
version = "2.1.0"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -334,7 +334,7 @@ dev = [
[[package]]
name = "langgraph-checkpoint-postgres"
version = "2.0.23"
version = "2.0.22"
source = { editable = "." }
dependencies = [
{ name = "langgraph-checkpoint" },
@@ -381,7 +381,7 @@ dev = [
[[package]]
name = "langsmith"
version = "0.4.5"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -392,9 +392,9 @@ dependencies = [
{ name = "requests-toolbelt" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/92/7885823f3d13222f57773921f0da19b37d628c64607491233dc853a0f6ea/langsmith-0.4.5.tar.gz", hash = "sha256:49444bd8ccd4e46402f1b9ff1d686fa8e3a31b175e7085e72175ab8ec6164a34", size = 352235, upload-time = "2025-07-10T22:08:04.505Z" }
sdist = { url = "https://files.pythonhosted.org/packages/20/c8/8d2e0fc438d2d3d8d4300f7684ea30a754344ed00d7ba9cc2705241d2a5f/langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66", size = 352105, upload-time = "2025-06-27T19:20:36.207Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/10/ad3107b666c3203b7938d10ea6b8746b9735c399cf737a51386d58e41d34/langsmith-0.4.5-py3-none-any.whl", hash = "sha256:4167717a2cccc4dff5809dbddc439628e836f6fd13d4fdb31ea013bc8d5cfaf5", size = 367795, upload-time = "2025-07-10T22:08:02.548Z" },
{ url = "https://files.pythonhosted.org/packages/1d/33/a3337eb70d795495a299a1640d7a75f17fb917155a64309b96106e7b9452/langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716", size = 367687, upload-time = "2025-06-27T19:20:33.839Z" },
]
[[package]]
@@ -977,27 +977,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.12.3"
version = "0.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/2a/43955b530c49684d3c38fcda18c43caf91e99204c2a065552528e0552d4f/ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77", size = 4459341, upload-time = "2025-07-11T13:21:16.086Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/fd/b44c5115539de0d598d75232a1cc7201430b6891808df111b8b0506aae43/ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2", size = 10430499, upload-time = "2025-07-11T13:20:26.321Z" },
{ url = "https://files.pythonhosted.org/packages/43/c5/9eba4f337970d7f639a37077be067e4ec80a2ad359e4cc6c5b56805cbc66/ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041", size = 11213413, upload-time = "2025-07-11T13:20:30.017Z" },
{ url = "https://files.pythonhosted.org/packages/e2/2c/fac3016236cf1fe0bdc8e5de4f24c76ce53c6dd9b5f350d902549b7719b2/ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882", size = 10586941, upload-time = "2025-07-11T13:20:33.046Z" },
{ url = "https://files.pythonhosted.org/packages/c5/0f/41fec224e9dfa49a139f0b402ad6f5d53696ba1800e0f77b279d55210ca9/ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901", size = 10783001, upload-time = "2025-07-11T13:20:35.534Z" },
{ url = "https://files.pythonhosted.org/packages/0d/ca/dd64a9ce56d9ed6cad109606ac014860b1c217c883e93bf61536400ba107/ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0", size = 10269641, upload-time = "2025-07-11T13:20:38.459Z" },
{ url = "https://files.pythonhosted.org/packages/63/5c/2be545034c6bd5ce5bb740ced3e7014d7916f4c445974be11d2a406d5088/ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6", size = 11875059, upload-time = "2025-07-11T13:20:41.517Z" },
{ url = "https://files.pythonhosted.org/packages/8e/d4/a74ef1e801ceb5855e9527dae105eaff136afcb9cc4d2056d44feb0e4792/ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc", size = 12658890, upload-time = "2025-07-11T13:20:44.442Z" },
{ url = "https://files.pythonhosted.org/packages/13/c8/1057916416de02e6d7c9bcd550868a49b72df94e3cca0aeb77457dcd9644/ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687", size = 12232008, upload-time = "2025-07-11T13:20:47.374Z" },
{ url = "https://files.pythonhosted.org/packages/f5/59/4f7c130cc25220392051fadfe15f63ed70001487eca21d1796db46cbcc04/ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e", size = 11499096, upload-time = "2025-07-11T13:20:50.348Z" },
{ url = "https://files.pythonhosted.org/packages/d4/01/a0ad24a5d2ed6be03a312e30d32d4e3904bfdbc1cdbe63c47be9d0e82c79/ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311", size = 11688307, upload-time = "2025-07-11T13:20:52.945Z" },
{ url = "https://files.pythonhosted.org/packages/93/72/08f9e826085b1f57c9a0226e48acb27643ff19b61516a34c6cab9d6ff3fa/ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07", size = 10661020, upload-time = "2025-07-11T13:20:55.799Z" },
{ url = "https://files.pythonhosted.org/packages/80/a0/68da1250d12893466c78e54b4a0ff381370a33d848804bb51279367fc688/ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12", size = 10246300, upload-time = "2025-07-11T13:20:58.222Z" },
{ url = "https://files.pythonhosted.org/packages/6a/22/5f0093d556403e04b6fd0984fc0fb32fbb6f6ce116828fd54306a946f444/ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b", size = 11263119, upload-time = "2025-07-11T13:21:01.503Z" },
{ url = "https://files.pythonhosted.org/packages/92/c9/f4c0b69bdaffb9968ba40dd5fa7df354ae0c73d01f988601d8fac0c639b1/ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f", size = 11746990, upload-time = "2025-07-11T13:21:04.524Z" },
{ url = "https://files.pythonhosted.org/packages/fe/84/7cc7bd73924ee6be4724be0db5414a4a2ed82d06b30827342315a1be9e9c/ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d", size = 10589263, upload-time = "2025-07-11T13:21:07.148Z" },
{ url = "https://files.pythonhosted.org/packages/07/87/c070f5f027bd81f3efee7d14cb4d84067ecf67a3a8efb43aadfc72aa79a6/ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7", size = 11695072, upload-time = "2025-07-11T13:21:11.004Z" },
{ url = "https://files.pythonhosted.org/packages/e0/30/f3eaf6563c637b6e66238ed6535f6775480db973c836336e4122161986fc/ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1", size = 10805855, upload-time = "2025-07-11T13:21:13.547Z" },
{ url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" },
{ url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" },
{ url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" },
{ url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" },
{ url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" },
{ url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" },
{ url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" },
{ url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" },
{ url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" },
{ url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" },
{ url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" },
{ url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" },
{ url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" },
{ url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" },
{ url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" },
{ url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" },
{ url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
]
[[package]]
@@ -29,7 +29,7 @@ _AIO_ERROR_MSG = (
"from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver\n"
"Note: AsyncSqliteSaver requires the aiosqlite package to use.\n"
"Install with:\n`pip install aiosqlite`\n"
"See https://langchain-ai.github.io/langgraph/reference/checkpoints/#langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver"
"See https://langchain-ai.github.io/langgraph/reference/checkpoints/asyncsqlitesaver"
"for more information."
)
@@ -3,7 +3,6 @@ from __future__ import annotations
import concurrent.futures
import datetime
import logging
import re
import sqlite3
import threading
from collections import defaultdict
@@ -108,23 +107,6 @@ def _decode_ns_text(namespace: str) -> tuple[str, ...]:
return tuple(namespace.split("."))
def _validate_filter_key(key: str) -> None:
"""Validate that a filter key is safe for use in SQL queries.
Args:
key: The filter key to validate
Raises:
ValueError: If the key contains invalid characters that could enable SQL injection
"""
# Allow alphanumeric characters, underscores, dots, and hyphens
# This covers typical JSON property names while preventing SQL injection
if not re.match(r"^[a-zA-Z0-9_.-]+$", key):
raise ValueError(
f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
)
def _json_loads(content: bytes | str | orjson.Fragment) -> Any:
if isinstance(content, orjson.Fragment):
if hasattr(content, "buf"):
@@ -390,8 +372,6 @@ class BaseSqliteStore:
filter_conditions = []
if op.filter:
for key, value in op.filter.items():
_validate_filter_key(key)
if isinstance(value, dict):
for op_name, val in value.items():
condition, filter_params_ = self._get_filter_condition(
@@ -642,8 +622,6 @@ class BaseSqliteStore:
def _get_filter_condition(self, key: str, op: str, value: Any) -> tuple[str, list]:
"""Helper to generate filter conditions."""
_validate_filter_key(key)
# We need to properly format values for SQLite JSON extraction comparison
if op == "$eq":
if isinstance(value, str):
@@ -880,8 +858,6 @@ class SqliteStore(BaseSqliteStore, BaseStore):
def _get_filter_condition(self, key: str, op: str, value: Any) -> tuple[str, list]:
"""Helper to generate filter conditions."""
_validate_filter_key(key)
# We need to properly format values for SQLite JSON extraction comparison
if op == "$eq":
if isinstance(value, str):
@@ -19,7 +19,8 @@ class TestAsyncSqliteSaver:
self.config_1: RunnableConfig = {
"configurable": {
"thread_id": "thread-1",
"checkpoint_id": "1",
# for backwards compatibility testing
"thread_ts": "1",
"checkpoint_ns": "",
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ class TestSqliteSaver:
"configurable": {
"thread_id": "thread-1",
# for backwards compatibility testing
"checkpoint_id": "1",
"thread_ts": "1",
"checkpoint_ns": "",
}
}
@@ -1047,23 +1047,3 @@ def test_search_items(
for ns in test_namespaces:
key = f"item_{ns[-1]}"
store.delete(ns, key)
def test_sql_injection_vulnerability(store: SqliteStore) -> None:
"""Test that SQL injection via malicious filter keys is prevented."""
# Add public and private documents
store.put(("docs",), "public", {"access": "public", "data": "public info"})
store.put(
("docs",), "private", {"access": "private", "data": "secret", "password": "123"}
)
# Normal query - returns 1 public document
normal = store.search(("docs",), filter={"access": "public"})
assert len(normal) == 1
assert normal[0].value["access"] == "public"
# SQL injection attempt via malicious key should raise ValueError
malicious_key = "access') = 'public' OR '1'='1' OR json_extract(value, '$."
with pytest.raises(ValueError, match="Invalid filter key"):
store.search(("docs",), filter={malicious_key: "dummy"})
+26 -26
View File
@@ -40,11 +40,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2025.7.9"
version = "2025.6.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" }
sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" },
{ url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
]
[[package]]
@@ -316,7 +316,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "2.1.1"
version = "2.1.0"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -389,7 +389,7 @@ dev = [
[[package]]
name = "langsmith"
version = "0.4.5"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -400,9 +400,9 @@ dependencies = [
{ name = "requests-toolbelt" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/92/7885823f3d13222f57773921f0da19b37d628c64607491233dc853a0f6ea/langsmith-0.4.5.tar.gz", hash = "sha256:49444bd8ccd4e46402f1b9ff1d686fa8e3a31b175e7085e72175ab8ec6164a34", size = 352235, upload-time = "2025-07-10T22:08:04.505Z" }
sdist = { url = "https://files.pythonhosted.org/packages/20/c8/8d2e0fc438d2d3d8d4300f7684ea30a754344ed00d7ba9cc2705241d2a5f/langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66", size = 352105, upload-time = "2025-06-27T19:20:36.207Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/10/ad3107b666c3203b7938d10ea6b8746b9735c399cf737a51386d58e41d34/langsmith-0.4.5-py3-none-any.whl", hash = "sha256:4167717a2cccc4dff5809dbddc439628e836f6fd13d4fdb31ea013bc8d5cfaf5", size = 367795, upload-time = "2025-07-10T22:08:02.548Z" },
{ url = "https://files.pythonhosted.org/packages/1d/33/a3337eb70d795495a299a1640d7a75f17fb917155a64309b96106e7b9452/langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716", size = 367687, upload-time = "2025-06-27T19:20:33.839Z" },
]
[[package]]
@@ -905,27 +905,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.12.3"
version = "0.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/2a/43955b530c49684d3c38fcda18c43caf91e99204c2a065552528e0552d4f/ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77", size = 4459341, upload-time = "2025-07-11T13:21:16.086Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/fd/b44c5115539de0d598d75232a1cc7201430b6891808df111b8b0506aae43/ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2", size = 10430499, upload-time = "2025-07-11T13:20:26.321Z" },
{ url = "https://files.pythonhosted.org/packages/43/c5/9eba4f337970d7f639a37077be067e4ec80a2ad359e4cc6c5b56805cbc66/ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041", size = 11213413, upload-time = "2025-07-11T13:20:30.017Z" },
{ url = "https://files.pythonhosted.org/packages/e2/2c/fac3016236cf1fe0bdc8e5de4f24c76ce53c6dd9b5f350d902549b7719b2/ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882", size = 10586941, upload-time = "2025-07-11T13:20:33.046Z" },
{ url = "https://files.pythonhosted.org/packages/c5/0f/41fec224e9dfa49a139f0b402ad6f5d53696ba1800e0f77b279d55210ca9/ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901", size = 10783001, upload-time = "2025-07-11T13:20:35.534Z" },
{ url = "https://files.pythonhosted.org/packages/0d/ca/dd64a9ce56d9ed6cad109606ac014860b1c217c883e93bf61536400ba107/ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0", size = 10269641, upload-time = "2025-07-11T13:20:38.459Z" },
{ url = "https://files.pythonhosted.org/packages/63/5c/2be545034c6bd5ce5bb740ced3e7014d7916f4c445974be11d2a406d5088/ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6", size = 11875059, upload-time = "2025-07-11T13:20:41.517Z" },
{ url = "https://files.pythonhosted.org/packages/8e/d4/a74ef1e801ceb5855e9527dae105eaff136afcb9cc4d2056d44feb0e4792/ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc", size = 12658890, upload-time = "2025-07-11T13:20:44.442Z" },
{ url = "https://files.pythonhosted.org/packages/13/c8/1057916416de02e6d7c9bcd550868a49b72df94e3cca0aeb77457dcd9644/ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687", size = 12232008, upload-time = "2025-07-11T13:20:47.374Z" },
{ url = "https://files.pythonhosted.org/packages/f5/59/4f7c130cc25220392051fadfe15f63ed70001487eca21d1796db46cbcc04/ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e", size = 11499096, upload-time = "2025-07-11T13:20:50.348Z" },
{ url = "https://files.pythonhosted.org/packages/d4/01/a0ad24a5d2ed6be03a312e30d32d4e3904bfdbc1cdbe63c47be9d0e82c79/ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311", size = 11688307, upload-time = "2025-07-11T13:20:52.945Z" },
{ url = "https://files.pythonhosted.org/packages/93/72/08f9e826085b1f57c9a0226e48acb27643ff19b61516a34c6cab9d6ff3fa/ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07", size = 10661020, upload-time = "2025-07-11T13:20:55.799Z" },
{ url = "https://files.pythonhosted.org/packages/80/a0/68da1250d12893466c78e54b4a0ff381370a33d848804bb51279367fc688/ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12", size = 10246300, upload-time = "2025-07-11T13:20:58.222Z" },
{ url = "https://files.pythonhosted.org/packages/6a/22/5f0093d556403e04b6fd0984fc0fb32fbb6f6ce116828fd54306a946f444/ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b", size = 11263119, upload-time = "2025-07-11T13:21:01.503Z" },
{ url = "https://files.pythonhosted.org/packages/92/c9/f4c0b69bdaffb9968ba40dd5fa7df354ae0c73d01f988601d8fac0c639b1/ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f", size = 11746990, upload-time = "2025-07-11T13:21:04.524Z" },
{ url = "https://files.pythonhosted.org/packages/fe/84/7cc7bd73924ee6be4724be0db5414a4a2ed82d06b30827342315a1be9e9c/ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d", size = 10589263, upload-time = "2025-07-11T13:21:07.148Z" },
{ url = "https://files.pythonhosted.org/packages/07/87/c070f5f027bd81f3efee7d14cb4d84067ecf67a3a8efb43aadfc72aa79a6/ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7", size = 11695072, upload-time = "2025-07-11T13:21:11.004Z" },
{ url = "https://files.pythonhosted.org/packages/e0/30/f3eaf6563c637b6e66238ed6535f6775480db973c836336e4122161986fc/ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1", size = 10805855, upload-time = "2025-07-11T13:21:13.547Z" },
{ url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" },
{ url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" },
{ url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" },
{ url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" },
{ url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" },
{ url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" },
{ url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" },
{ url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" },
{ url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" },
{ url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" },
{ url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" },
{ url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" },
{ url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" },
{ url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" },
{ url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" },
{ url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" },
{ url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
]
[[package]]
+3 -3
View File
@@ -36,7 +36,7 @@ Each checkpointer should conform to `langgraph.checkpoint.base.BaseCheckpointSav
- `.put` - Store a checkpoint with its configuration and metadata.
- `.put_writes` - Store intermediate writes linked to a checkpoint (i.e. pending writes).
- `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`).
- `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `thread_ts`).
- `.list` - List checkpoints that match a given configuration and filter criteria.
If the checkpointer will be used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), checkpointer must implement asynchronous versions of the above methods (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`).
@@ -44,12 +44,12 @@ If the checkpointer will be used with asynchronous graph execution (i.e. executi
## Usage
```python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.memory import MemorySaver
write_config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
read_config = {"configurable": {"thread_id": "1"}}
checkpointer = InMemorySaver()
checkpointer = MemorySaver()
checkpoint = {
"v": 4,
"ts": "2024-07-31T20:14:19.804150+00:00",
@@ -375,8 +375,10 @@ class EmptyChannelError(Exception):
def get_checkpoint_id(config: RunnableConfig) -> str | None:
"""Get checkpoint ID."""
return config["configurable"].get("checkpoint_id")
"""Get checkpoint ID in a backwards-compatible manner (fallback on thread_ts)."""
return config["configurable"].get(
"checkpoint_id", config["configurable"].get("thread_ts")
)
def get_checkpoint_metadata(
@@ -411,6 +413,7 @@ WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2, INTERRUPT: -3, RESUME: -4}
EXCLUDED_METADATA_KEYS = {
"thread_id",
"thread_ts",
"checkpoint_id",
"checkpoint_ns",
"checkpoint_map",
@@ -595,6 +595,6 @@ class PersistentDict(defaultdict):
except EOFError:
return
except Exception:
logger.error(f"Failed to load file: {fileobj.name}")
logging.error(f"Failed to load file: {fileobj.name}")
raise
raise ValueError("File not in a supported format")
@@ -343,14 +343,10 @@ async def _run(
# set the results of each operation
for fut, result in zip(futs, results):
# guard against future being done (e.g. cancelled)
if not fut.done():
fut.set_result(result)
fut.set_result(result)
except Exception as e:
for fut in futs:
# guard against future being done (e.g. cancelled)
if not fut.done():
fut.set_exception(e)
fut.set_exception(e)
finally:
# remove strong ref to store
del s

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