Compare commits

..
Author SHA1 Message Date
William Fu-Hinthorn 366323b49b Add custom router support in langgraph-api 2025-02-17 11:44:01 -08:00
David DuongandGitHub c44ec55095 fix(docs): broken assistant-ui link (#3467) 2025-02-17 04:50:11 +01:00
Tat Dat Duong 25d682cc9e fix(docs): broken assistant-ui link 2025-02-17 04:38:22 +01:00
Vadym BardaandGitHub 6f37330141 langgraph: release 0.2.73 (#3456) 2025-02-15 16:13:30 -05:00
Vadym BardaandGitHub 1356a0ba42 langgraph: better typing for node functions (#3455) 2025-02-15 21:11:33 +00:00
Nuno Campos 9786be1ff7 Update requirement 2025-02-14 19:01:50 -08:00
Nuno CamposandGitHub c2a129c882 Exclude complex values from checkpoint metadata (#3448) 2025-02-14 18:51:48 -08:00
Nuno Campos 62f004fd28 Lint 2025-02-14 18:42:40 -08:00
Nuno Campos d4b22ac1d4 Fix postgres tests 2025-02-14 18:40:30 -08:00
Nuno Campos 0415c02b40 Fix sqlite tests 2025-02-14 18:34:06 -08:00
Nuno Campos e8665f84e7 Update 2025-02-14 18:31:03 -08:00
Nuno Campos 8ff5e79e70 Lint 2025-02-14 18:27:49 -08:00
Nuno Campos 7f4822931e Update tests 2025-02-14 18:24:48 -08:00
Nuno Campos 9706211aca Exclude complex values from checkpoint metadata 2025-02-14 17:35:14 -08:00
Ben BurnsandGitHub 405da6d507 chore(docs): enable analytics, add consent banner & copyright notice (#3447) 2025-02-15 00:54:31 +00:00
Nuno Campos bf7252cadc checkpoint 2.0.14 2025-02-14 12:28:04 -08:00
Nuno CamposandGitHub e33bac6737 Fix busy loop in AsyncBatchedBaseStore (#3445)
- while loop w asyncio.sleep(0) takes up cpu
2025-02-14 12:12:28 -08:00
Nuno Campos da97d2e1ba Fix 2025-02-14 12:03:03 -08:00
Nuno Campos 6baf320d8e Fix 2025-02-14 10:43:03 -08:00
Nuno Campos a064ccdca1 Lint 2025-02-14 10:40:09 -08:00
Nuno Campos b5479b48bf Lint 2025-02-14 10:39:34 -08:00
Nuno Campos 9dbcb03185 Fix busy loop in AsyncBatchedBaseStore
- while loop w asyncio.sleep(0) takes up cpu
2025-02-14 10:36:02 -08:00
Eugene YurtsevandGitHub f2faa39ca9 add api reference for Pregel (#3437) 2025-02-13 21:07:43 -05:00
Eugene YurtsevandGitHub b4f6cdf01f docs: remove astream events from streaming conceptual guide (#3438) 2025-02-13 21:07:30 -05:00
Vadym BardaandGitHub cd976e779d docs: update stateless runs how-to guide (#3439) 2025-02-13 20:27:02 -05:00
Eugene YurtsevandGitHub c1f337f50b docs: add check code output for result="ansi" (#3435)
* Add ast parsing to determine whether we should include result="ansi".
It's not meant to be perfect, but will hopefully catch the most common
cases. Still requires manual review.
* Ideally we could suppress output in markdown-exec in the future.
2025-02-14 01:07:40 +00:00
David DuongandGitHub 31d3ceaf6d feat(sdk-js): bump to 0.0.42 (#3436) 2025-02-13 16:27:04 -08:00
Tat Dat Duong a48844632d feat(sdk-js): bump to 0.0.42 2025-02-13 16:17:02 -08:00
Eugene YurtsevandGitHub 437891aa4f docs: handle more links (#3434) 2025-02-13 23:43:29 +00:00
Eugene YurtsevandGitHub 3e1bbd3123 docs: cell magic to shell block conversion (#3433)
* add handling for blocks that use magic commands like %pip to convert
them into bash
* Apply new logic to another notebook
2025-02-13 18:23:49 -05:00
Eugene YurtsevandGitHub 9111449ffd docs: handle input() and cell magic for notebook conversion (#3432) 2025-02-13 22:29:32 +00:00
Eugene YurtsevandGitHub 77d7c00ce8 docs: fix up edge cases with links in notebooks (#3414)
* Adds another notebook conversion
* Fix up some edge cases for handling links in notebooks. Notebooks
links were using a different convention than markdown links.

We'll need to push additional logic to use an appropriate suffix (.md or
.ipynb) for cross-references between how-to guides (though these should
be rare).
2025-02-13 15:33:55 -05:00
Eugene YurtsevandGitHub 91725d742d docs: add unit tests to build pipeline (#3427)
* Add testing step to to docs build pipeline
* Requires updating import structure in some place
* Add simple unit test to cover some logic with highlights
2025-02-13 15:32:42 -05:00
David DuongandGitHub d73a4539ec feat(sdk-js): add docs for new useStream hook (#3420)
- **Add basic docs**
- **Add docs**
- **feat(sdk-js): add docs, how-to guide**
2025-02-13 11:32:03 -08:00
Nuno CamposandGitHub 15e2df6da5 Add LGP Arch page (#3428) 2025-02-13 11:22:29 -08:00
Tat Dat Duong ce6b396186 Update index page as well 2025-02-13 11:16:32 -08:00
Tat Dat Duong d2ab02edf1 Add shoutout to CopilotKit and assistant-ui 2025-02-13 11:15:45 -08:00
Nuno Campos 0aef9424a8 Add link 2025-02-13 11:12:49 -08:00
Nuno Campos ed23288e5b Add LGP Arch page 2025-02-13 11:09:31 -08:00
Tat Dat Duong 7ec8a4cb4d Update type definitions 2025-02-13 10:57:00 -08:00
David DuongandGitHub 2d9ca3045e feat(sdk-js): expose branches (#3426) 2025-02-13 10:45:01 -08:00
David DuongandGitHub b065c54871 feat(sdk-js): make "messages" the default key (#3425) 2025-02-13 10:39:17 -08:00
Tat Dat Duong 3c0a677c90 feat(sdk-js): make "messages" the default key 2025-02-13 10:36:22 -08:00
Tat Dat Duong 7e4852373d Fix broken links 2025-02-13 10:23:11 -08:00
Tat Dat Duong 422b2ba7f0 Update docs 2025-02-13 10:18:09 -08:00
Tat Dat Duong 2c66ac869d feat(sdk-js): expose branches 2025-02-13 10:17:42 -08:00
Matt SteadmanandGitHub 1ef7121100 docs: Fix grammar in LangGraph Glossery (#3407) 2025-02-13 13:05:54 -05:00
William FHandGitHub a53287f3d8 Clarify custom auth <-> deployment options (#3423) 2025-02-13 17:38:38 +00:00
Vadym BardaandGitHub da96925ecb ci: install mkdocs-insiders only for internal PRs (#3422) 2025-02-13 12:24:30 -05:00
Tat Dat Duong f3403eab48 Add link ref 2025-02-13 07:55:46 -08:00
Tat Dat Duong 208d9d165d feat(sdk-js): add docs, how-to guide 2025-02-13 07:44:01 -08:00
Tat Dat Duong 49c74dd569 Add docs 2025-02-13 07:12:15 -08:00
Tat Dat Duong 2d97af57f8 Add basic docs 2025-02-13 07:08:35 -08:00
b310ce07bc docs: add scripts for notebook conversion (#3406)
* Update notebook conversion code
* Convert one more file

---------

Co-authored-by: Ben Burns <803016+benjamincburns@users.noreply.github.com>
2025-02-13 06:48:27 +00:00
Vadym BardaandGitHub 65976f311f langgraph: release 0.2.72 (#3413) 2025-02-13 00:14:54 -05:00
Nuno CamposandGitHub 80c9d61fbd langgraph: fix None handling for pydantic state updates (#3411) 2025-02-12 21:05:02 -08:00
ZapironandGitHub a578c7b137 docs: fix grammatical error for navigating node to parent section (#3356)
Add within to better explain moving from one subgraph to another
2025-02-12 23:59:59 -05:00
vbarda 04e8342d97 pydantic v1 2025-02-12 20:46:12 -08:00
vbarda b0e11ae524 update 2025-02-12 20:02:26 -08:00
vbarda c36323cba8 3.9 2025-02-12 19:17:05 -08:00
vbarda 661476e88d langgraph: fix None handling for pydantic state updates 2025-02-12 17:53:12 -08:00
72 changed files with 3783 additions and 2628 deletions
+9 -1
View File
@@ -78,15 +78,23 @@ jobs:
pytest \
pytest-check-links \
GitPython \
"git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git" \
"git+https://github.com/benjamincburns/markdown-exec.git@cc0d39d737e5ffd4b83d23cd8729d7ea16e363c8"
# we run this installation only for internal PRs
# as GITHUB_TOKEN is not available for PRs from outside contributors
if [ -n "${GITHUB_TOKEN}" ]; then
poetry run pip install "git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
fi
poetry run jupyter kernelspec list
poetry run python3 -m ipykernel install --user --name=python3
npm install -g tslab
poetry run tslab install --python=python3
poetry run jupyter kernelspec list
- name: Run unit tests
# Run unit tests on the docs build pipeline
run: make tests
- name: Lint Docs
# This step lints the docs using the existing linting set up.
# It should be very fast and should not require any external services.
+7 -2
View File
@@ -1,4 +1,4 @@
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc llms-text build-prebuilt
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc llms-text build-prebuilt tests
build-typedoc:
cd ../libs/sdk-js && yarn install --include-dev && yarn typedoc
@@ -17,7 +17,7 @@ build-docs: build-typedoc build-prebuilt
poetry run python -m mkdocs build --clean -f mkdocs.yml --strict
llms-text:
poetry run python _scripts/generate_llms_text.py docs/llms-full.txt
poetry run python -m _scripts.generate_llms_text docs/llms-full.txt
install-vercel-deps:
dnf install -y python3.11
@@ -33,6 +33,11 @@ install-vercel-deps:
poetry run jupyter kernelspec list
tests:
# RUn unit tests
poetry run pytest tests/unit_tests
vercel-build-docs: install-vercel-deps
make build-docs
+75 -22
View File
@@ -1,8 +1,11 @@
import importlib
from importlib.machinery import ModuleSpec
import importlib.util
import inspect
import logging
import re
from functools import lru_cache
import sys
from typing import List, Literal, Optional
from typing_extensions import TypedDict
@@ -72,9 +75,8 @@ def _make_regular_expression(pkg_prefix: str) -> re.Pattern:
if not pkg_prefix.isidentifier():
raise ValueError(f"Invalid package prefix: {pkg_prefix}")
return re.compile(
r"from\s+(" + pkg_prefix + "(?:_\w+)?(?:\.\w+)*?)\s+import\s+"
r"((?:\w+(?:,\s*)?)*" # Match zero or more words separated by a comma+optional ws
r"(?:\s*\(.*?\))?)", # Match optional parentheses block
r"from\s+(" + pkg_prefix + r"(?:_\w+)?(?:\.\w+)*?)\s+import\s+\(?"
r"((?:\w+(?:,\s*)?)*)\s*\)?", # Match zero or more words separated by a comma+optional ws
re.DOTALL, # Match newlines as well
)
@@ -85,22 +87,57 @@ _IMPORT_LANGGRAPH_RE = _make_regular_expression("langgraph")
@lru_cache(maxsize=10_000)
def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
def _get_full_module_name(
module_path: str, class_name: str | None, doc_title: str
) -> Optional[str]:
"""Get full module name using inspect, with LRU cache to memoize results."""
try:
module = importlib.import_module(module_path)
class_ = getattr(module, class_name)
module = inspect.getmodule(class_)
if module is None:
# For constants, inspect.getmodule() might return None
# In this case, we'll return the original module_path
return module_path
if module_path in sys.modules:
module = sys.modules[module_path]
else:
spec: ModuleSpec | None = importlib.util.find_spec(module_path)
if spec is not None:
module = importlib.util.module_from_spec(spec)
sys.modules[module_path] = module
spec.loader.exec_module(module)
if class_name is not None:
class_ = getattr(module, class_name)
if re.match(r"\w+\s+as\s+\w+", class_name):
# Handle cases like "A as B"
class_name, _ = class_name.split(" as ")
module = inspect.getmodule(class_)
if module is None:
# For constants, inspect.getmodule() might return None
# In this case, we'll return the original module_path
return module_path
return module.__name__
except AttributeError as e:
logger.warning(f"API Reference: Could not find module for {class_name}, {e}")
if class_name is not None:
# the class_name might actually be a module
# e.g. from langchain import hub
# try to import it as a module, and if that doesn't work, throw
if class_name is not None:
module_name = _get_full_module_name(
f"{module_path}.{class_name}", None, doc_title
)
if module_name is not None:
# return the name of the parent module, rather than the name of the class as though it were a module
return module.__name__
logger.warning(
f"API Reference: Could not find module for {class_name} in {module_path}, imported in doc {doc_title}, {e}"
)
# don't log if we're trying to import the "hub" part as though it were a module
logger.warning(
f"API Reference: Could not find module for {module_path}, imported in doc {doc_title}, {e}"
)
return None
except ImportError as e:
logger.warning(f"API Reference: Failed to load for class {class_name}, {e}")
logger.warning(
f"API Reference: Failed to import module {module_path} {doc_title}, {e}"
)
return None
@@ -160,7 +197,22 @@ def _get_imports(
if imp.strip()
]
for class_name in imported_classes:
module_path = _get_full_module_name(module, class_name)
if module == "langchain_core.messages" and class_name == ")":
print("WARNING: ", file=sys.stderr)
print(
f"WARNING: Trying to import {class_name} from {module} in doc {doc_title}",
file=sys.stderr,
)
print("WARNING: ", file=sys.stderr)
print("WARNING:", import_match.group(0), file=sys.stderr)
print("WARNING: ", file=sys.stderr)
print(
"\n".join([f"WARNING: {line}" for line in code.splitlines()]),
file=sys.stderr,
)
print("WARNING: ", file=sys.stderr)
module_path = _get_full_module_name(module, class_name, doc_title)
if not module_path:
continue
if len(module_path.split(".")) < 2:
@@ -230,7 +282,7 @@ def get_imports(code: str, doc_title: str) -> List[ImportInformation]:
return all_imports
def update_markdown_with_imports(markdown: str) -> str:
def update_markdown_with_imports(markdown: str, file_name: str) -> str:
"""Update markdown to include API reference links for imports in Python code blocks.
This function scans the markdown content for Python code blocks, extracts any imports, and appends links to their API documentation.
@@ -250,7 +302,8 @@ def update_markdown_with_imports(markdown: str) -> str:
This function will append an API reference link to the `TextGenerator` class from the `langchain.nlp` module if it's recognized.
"""
code_block_pattern = re.compile(
r'(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```', re.DOTALL
r"(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```",
re.DOTALL,
)
def replace_code_block(match: re.Match) -> str:
@@ -262,11 +315,11 @@ def update_markdown_with_imports(markdown: str) -> str:
Returns:
str: The modified code block with API reference links appended if applicable.
"""
indent = match.group('indent')
code_block = match.group('code')
language = match.group('language') # Preserve the language from the regex match
indent = match.group("indent")
code_block = match.group("code")
language = match.group("language") # Preserve the language from the regex match
# Retrieve import information from the code block
imports = get_imports(code_block, "__unused__")
imports = get_imports(code_block, file_name)
original_code_block = match.group(0)
# If no imports are found, return the original code block
@@ -274,11 +327,11 @@ def update_markdown_with_imports(markdown: str) -> str:
return original_code_block
# Generate API reference links for each import
api_links = ' | '.join(
api_links = " | ".join(
f'<a href="{imp["docs"]}">{imp["imported"]}</a>' for imp in imports
)
# Return the code block with appended API reference links
return f'{original_code_block}\n\n{indent}API Reference: {api_links}'
return f"{original_code_block}\n\n{indent}API Reference: {api_links}"
# Apply the replace_code_block function to all matches in the markdown
updated_markdown = code_block_pattern.sub(replace_code_block, markdown)
+1 -2
View File
@@ -2,12 +2,11 @@
import glob
import os
import pathlib
from mkdocs.structure.files import File
from mkdocs.structure.pages import Page
from notebook_hooks import _on_page_markdown_with_config
from _scripts.notebook_hooks import _on_page_markdown_with_config
HERE = os.path.dirname(os.path.abspath(__file__))
# Get source directory (parent of HERE / docs)
+241 -19
View File
@@ -1,4 +1,6 @@
import argparse
import ast
import glob
import os
import re
from pathlib import Path
@@ -9,28 +11,230 @@ from nbconvert.exporters import MarkdownExporter
from nbconvert.preprocessors import Preprocessor
def _uses_input(source: str) -> bool:
"""Parse the source code to determine if it uses the input() function."""
try:
tree = ast.parse(source)
except SyntaxError:
# If there's a syntax error, assume input() might be present to be safe.
return False
for node in ast.walk(tree):
if isinstance(node, ast.Call):
# Check if the function called is named 'input'
if isinstance(node.func, ast.Name) and node.func.id == "input":
return True
return False
def _rewrite_cell_magic(code: str) -> str:
"""Process a code block that uses cell magic.:w
- Lines starting with "%%capture" are ignored.
- Lines starting with "%pip" are rewritten by removing the leading "%" character.
- Any other non-empty line causes a NotImplementedError.
Args:
code (str): The original code block.
Returns:
str: The transformed code block.
Raises:
NotImplementedError: If a line doesn't start with either "%%capture" or "%pip".
"""
rewritten_lines = []
for line in code.splitlines():
stripped = line.strip()
# Skip empty lines
if not stripped:
continue
# Ignore %%capture lines
if stripped.startswith("%%capture"):
continue
# Rewrite %pip lines by dropping the '%'
elif stripped.startswith("%pip"):
# Drop the leading '%' character
rewritten_lines.append(stripped[1:])
# Anything else is not supported
else:
raise NotImplementedError(f"Unhandled line: {line}")
return "\n".join(rewritten_lines)
class PrintCallVisitor(ast.NodeVisitor):
"""
This visitor sets self.has_print to True if it encounters a call
to a print within the global scope.
This should catch calls to print(), print_stream(), etc. (Prefixed with "print").
May have some false positives, but it's not meant to be perfect.
Temporary code for notebook conversion.
"""
def __init__(self):
self.has_print = False
self.scope_level = 0 # counter to track whether we're inside a def/lambda
def visit_FunctionDef(self, node):
self.scope_level += 1
self.generic_visit(node)
self.scope_level -= 1
def visit_AsyncFunctionDef(self, node):
self.scope_level += 1
self.generic_visit(node)
self.scope_level -= 1
def visit_Lambda(self, node):
self.scope_level += 1
self.generic_visit(node)
self.scope_level -= 1
def visit_ClassDef(self, node):
self.scope_level += 1
self.generic_visit(node)
self.scope_level -= 1
def visit_Call(self, node):
# Only consider calls when not inside a function definition.
if self.scope_level == 0:
if isinstance(node.func, ast.Name) and node.func.id.startswith("print"):
self.has_print = True
self.generic_visit(node)
def _has_output(source: str) -> bool:
"""Determine if the code block is expected to produce output.
Args:
source (str): The source code of the code block.
Returns:
True if the code block is expected to produce output, False otherwise.
Must meet the following conditions:
1. There is a call to a printing function (name starts with "print")
that is not inside a function definition.
2. The last top-level statement is an expression that is valid if:
- It is any expression (including calls) AND
- It is NOT a call to `display(...)`.
`display` isn't handled currently by markdown-exec
"""
try:
tree = ast.parse(source)
except SyntaxError:
return False
# Condition (1): Check for a global print-like call.
visitor = PrintCallVisitor()
visitor.visit(tree)
condition_a = visitor.has_print
# Condition (2): Check the last top-level statement.
condition_b = False
if tree.body:
last_stmt = tree.body[-1]
if isinstance(last_stmt, ast.Expr):
# If the expression is a call, ensure it's not a call to "display"
if isinstance(last_stmt.value, ast.Call):
if (
isinstance(last_stmt.value.func, ast.Name)
and last_stmt.value.func.id == "display"
):
condition_b = False # exclude display-wrapped expressions
else:
condition_b = True
else:
# Any other expression qualifies.
condition_b = True
return condition_a or condition_b
def _convert_links_in_markdown(markdown: str) -> str:
"""Convert links present in notebook markdown cells to standardized format.
We want to update markdown links code cells by linking to markdown
files rather than assuming that the link is to the finalized HTML.
This code is needed temporarily since the markdown links that are present
in ipython notebooks do not follow the same conventions as regular markdown
files in mkdocs (which should link to a .md file).
"""
# Define the regex pattern in parts for clarity:
pattern = (
r"(?<!!)" # Negative lookbehind: ensure the link is not an image (i.e., doesn't start with "!")
r"\[" # Literal '[' indicating the start of the link text.
r"(?P<text>[^\]]*)" # Named group 'text': match any characters except ']', representing the link text.
r"\]" # Literal ']' indicating the end of the link text.
r"\(" # Literal '(' indicating the start of the URL.
r"(?![^\)]*//)" # Negative lookahead: ensure that the URL does not contain '//' (skip absolute URLs).
r"(?P<url>[^)]*)" # Named group 'url': match any characters except ')', representing the URL.
r"\)" # Literal ')' indicating the end of the URL.
)
def custom_replacement(match):
"""logic will correct the link format used in ipython notebooks
Ipython notebooks were being converted directly into HTML links
instead of markdown links that retain the markdown extension.
It needs to handle the following cases:
- optional fragments (e.g., `#section`)
e.g., `[text](url/#section)` -> `[text](url.md#section)`
e.g., `[text](url#section)` -> `[text](url.md#section)`
- relative paths (e.g., `../path/to/file`) need to be denested by 1 level
"""
text = match.group("text")
url = match.group("url")
if url.startswith("../"):
# we strip the "../" from the start of the URL
# We only need to denest one level.
url = url[3:]
url = url.rstrip("/") # Strip `/` from the end of the URL
# if url has a fragment
if "#" in url:
url, fragment = url.split("#")
url = url.rstrip("/")
# Strip `/` from the end of the URL
return f"[{text}]({url}.md#{fragment})"
# Otherwise add the .md extension
return f"[{text}]({url}.md)"
return re.sub(
pattern,
custom_replacement,
markdown,
)
class EscapePreprocessor(Preprocessor):
def __init__(self, rewrite_links: bool = True, **kwargs) -> None:
def __init__(self, markdown_exec_migration: bool = False, **kwargs) -> None:
super().__init__(**kwargs)
self.rewrite_links = rewrite_links
self.markdown_exec_migration = markdown_exec_migration
def preprocess_cell(self, cell, resources, cell_index):
if cell.cell_type == "markdown":
if self.rewrite_links:
# We'll need to adjust the logic for this to keep markdown format
# but link to markdown files rather than ipynb files.
if not self.markdown_exec_migration:
# Old logic is to convert ipynb links to HTML links
cell.source = re.sub(
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
r'<a href="\2">\1</a>',
cell.source,
)
else:
# Keep format but replace the .ipynb extension with .md
cell.source = re.sub(
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
r"[\1](\2.md)",
cell.source,
)
cell.source = _convert_links_in_markdown(cell.source)
# Fix image paths in <img> tags
cell.source = re.sub(
@@ -39,9 +243,19 @@ class EscapePreprocessor(Preprocessor):
elif cell.cell_type == "code":
# Determine if the cell has bash or cell magic
if cell.source.startswith("%") or cell.source.startswith("!"):
# update metadata to denote that it's not a python cell
cell.metadata["language_info"] = {"name": "unknown"}
source = cell.source
is_exec = not (
source.startswith("%") or source.startswith("!") or _uses_input(source)
)
cell.metadata["exec"] = is_exec
if self.markdown_exec_migration:
# For markdown exec migration we'll re-write cell magic as bash commands
if source.startswith("%%"):
cell.source = _rewrite_cell_magic(source)
cell.metadata["language"] = "shell"
cell.metadata["has_output"] = _has_output(source)
# Remove noqa comments
cell.source = re.sub(r"#\s*noqa.*$", "", cell.source, flags=re.MULTILINE)
@@ -141,7 +355,7 @@ exporter = MarkdownExporter(
md_executable = MarkdownExporter(
preprocessors=[
ExtractAttachmentsPreprocessor,
EscapePreprocessor(rewrite_links=False),
EscapePreprocessor(markdown_exec_migration=True),
],
template_name="md_executable",
extra_template_basedirs=[
@@ -181,11 +395,19 @@ def _convert_notebooks(
raise ValueError("Either --output_dir or --replace must be specified")
output_dir_path = DOCS if replace else Path(output_dir)
notebooks = list(DOCS.rglob(pattern))
file_names = [notebook.name for notebook in notebooks]
# Get the directory where the script was executed
base_dir = os.getcwd()
# Build the full search pattern using the current working directory as the base
full_pattern = os.path.join(base_dir, args.pattern)
for notebook in notebooks:
# Use glob with recursive search enabled
matching_files = glob.glob(full_pattern, recursive=True)
paths = [Path(file) for file in matching_files]
file_names = [notebook.name for notebook in paths]
for notebook in paths:
markdown = convert_notebook(notebook, mode="exec")
markdown_path = output_dir_path / notebook.relative_to(DOCS).with_suffix(".md")
markdown_path.parent.mkdir(parents=True, exist_ok=True)
@@ -215,7 +437,7 @@ def _convert_notebooks(
return match.group(0)
# Process all markdown files in the output directory.
for path in output_dir_path.rglob("*.md"):
for path in output_dir_path.rglob("**/*.md"):
with open(path, "r", encoding="utf-8") as f:
content = f.read()
new_content = re.sub(link_pattern, replace_link, content)
@@ -5,8 +5,10 @@
```
{%- if 'magics_language' in cell.metadata -%}
{{ cell.metadata.magics_language}}
{%- elif cell.metadata.get('language') == "shell" -%}
shell
{%- elif 'name' in nb.metadata.get('language_info', {}) -%}
{{ nb.metadata.language_info.name }} exec="on" source="above" session="1"
{{ nb.metadata.language_info.name }}{% if cell.metadata.exec|default(false) %} exec="on" source="above" session="1"{% if cell.metadata.has_output|default(false) %} result="ansi"{% endif %}{% endif %}
{%- endif %}
{{ cell.source}}
```
+45 -27
View File
@@ -1,21 +1,19 @@
import logging
import os
import posixpath
import re
import traceback
from typing import Any, Callable, Dict
from markdown import Markdown
from pymdownx.superfences import SuperFencesException
from markdown_exec.hooks import SessionHistoryEntry
from mkdocs.structure.files import Files, File
from mkdocs.structure.pages import Page
import posixpath
from markdown_exec.hooks import SessionHistoryEntry
from generate_api_reference_links import update_markdown_with_imports
from notebook_convert import convert_notebook
from setup_vcr import load_postamble, load_preamble, _hash_string
from pymdownx.superfences import SuperFencesException
from _scripts.generate_api_reference_links import update_markdown_with_imports
from _scripts.notebook_convert import convert_notebook
from _scripts.setup_vcr import load_postamble, load_preamble, _hash_string
logger = logging.getLogger(__name__)
logging.basicConfig()
@@ -101,7 +99,7 @@ def _highlight_code_blocks(markdown: str) -> str:
# existing hl_lines for Python and JavaScript
# Pattern to find code blocks with highlight comments, handling optional indentation
code_block_pattern = re.compile(
r"(?P<indent>[ \t]*)```(?P<language>py|python|js|javascript)(?!\s+hl_lines=)\n"
r"(?P<indent>[ \t]*)```(?P<language>\w+)[ ]*(?P<attributes>[^\n]*)\n"
r"(?P<code>((?:.*\n)*?))" # Capture the code inside the block using named group
r"(?P=indent)```" # Match closing backticks with the same indentation
)
@@ -110,6 +108,13 @@ def _highlight_code_blocks(markdown: str) -> str:
indent = match.group("indent")
language = match.group("language")
code_block = match.group("code")
attributes = match.group("attributes").rstrip()
# Account for a case where hl_lines is manually specified
if "hl_lines" in attributes:
# Return original code block
return match.group(0)
lines = code_block.split("\n")
highlighted_lines = []
@@ -135,20 +140,23 @@ def _highlight_code_blocks(markdown: str) -> str:
# Reconstruct the new code block
new_code_block = "\n".join(lines_to_keep)
# Construct the full code block that also includes
# the fenced code block syntax.
opening_fence = f"```{language}"
if attributes:
opening_fence += f" {attributes}"
if highlighted_lines:
return (
f'{indent}```{language} hl_lines="{" ".join(highlighted_lines)}"\n'
# The indent and terminating \n is already included in the code block
f"{new_code_block}"
f"{indent}```"
)
else:
return (
f"{indent}```{language}\n"
# The indent and terminating \n is already included in the code block
f"{new_code_block}"
f"{indent}```"
)
opening_fence += f" hl_lines=\"{' '.join(highlighted_lines)}\""
return (
# The indent and opening fence
f"{indent}{opening_fence}\n"
# The indent and terminating \n is already included in the code block
f"{new_code_block}"
f"{indent}```"
)
# Replace all code blocks in the markdown
markdown = code_block_pattern.sub(replace_highlight_comments, markdown)
@@ -183,7 +191,7 @@ def handle_vcr_setup(
id = _hash_string(code)
if session is not None and session != "":
logger.info(f"new session {session} on page {document_filename}")
logger.info(f"new {language} session {session} on page {document_filename}")
cassette_prefix = document_filename.replace(".md", "").replace(os.path.sep, "_")
@@ -212,10 +220,21 @@ def handle_vcr_setup(
wrapped_lines.append(load_postamble(language))
transformed_source = "\n".join(wrapped_lines)
# Propagate extras
keep_extras = {
key: value
for key, value in kwargs["extra"].items()
if key
in {
"hl_lines",
}
}
return dict(
transform_source=lambda code: (transformed_source, code),
id=id,
extra={},
extra=keep_extras,
)
except Exception as e:
raise SuperFencesException(traceback.format_exc()) from e
@@ -239,8 +258,7 @@ def handle_vcr_teardown(
if document_filename is None:
logger.warning(f"no document filename found while tearing down {session}!")
else:
logger.info(f"tearing down {session} on {document_filename}")
logger.info(traceback.format_stack())
logger.info(f"tearing down {language} {session} on {document_filename}")
kwargs = dict(
code=code,
@@ -274,7 +292,7 @@ def _on_page_markdown_with_config(
# Append API reference links to code blocks
if add_api_references:
markdown = update_markdown_with_imports(markdown)
markdown = update_markdown_with_imports(markdown, page.file.src_path)
# Apply highlight comments to code blocks
markdown = _highlight_code_blocks(markdown)
+2 -2
View File
@@ -99,7 +99,7 @@ We can stream the results of a stateless run in an almost identical fashion to h
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--url <DEPLOYMENT_URL>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
@@ -144,7 +144,7 @@ In addition to streaming, you can also wait for a stateless result by using the
```bash
curl --request POST \
--url <DEPLOYMENT_URL>/runs/runs/wait \
--url <DEPLOYMENT_URL>/runs/wait \
--header 'Content-Type: application/json' \
--data '{
"assistant_id": <ASSISTANT_IDD>,
+410
View File
@@ -0,0 +1,410 @@
# How to integrate LangGraph into your React application
!!! info "Prerequisites"
- [LangGraph Platform](../../concepts/langgraph_platform.md)
- [LangGraph Server](../../concepts/langgraph_server.md)
The `useStream()` React hook provides a seamless way to integrate LangGraph into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences.
Key features:
- Messages streaming: Handle a stream of message chunks to form a complete message
- Automatic state management for messages, loading states, and errors
- Conversation branching: Create alternate conversation paths from any point in the chat history
- UI-agnostic design - bring your own components and styling
Let's explore how to use `useStream()` in your React application.
The `useStream()` provides a solid foundation for creating bespoke chat experiences. For pre-built chat components and interfaces, we recommend checking out [CopilotKit](https://docs.copilotkit.ai/coagents/quickstart/langgraph) and [assistant-ui](https://www.assistant-ui.com/docs/runtimes/langgraph).
## Example
```tsx
"use client";
import { useStream } from "@langchain/langgraph-sdk/react";
import type { Message } from "@langchain/langgraph-sdk";
export default function App() {
const thread = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<div>
<div>
{thread.messages.map((message) => (
<div key={message.id}>{message.content as string}</div>
))}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const message = new FormData(form).get("message") as string;
form.reset();
thread.submit({ messages: [{ type: "human", content: message }] });
}}
>
<input type="text" name="message" />
{thread.isLoading ? (
<button key="stop" type="button" onClick={() => thread.stop()}>
Stop
</button>
) : (
<button key="submit" type="submit">
Send
</button>
)}
</form>
</div>
);
}
```
## Customizing Your UI
The `useStream()` hook takes care of all the complex state management behind the scenes, providing you with simple interfaces to build your UI. Here's what you get out of the box:
- Thread state management
- Loading and error states
- Message handling and updates
- Branching support
Here are some examples on how to use these features effectively:
### Loading States
The `isLoading` property tells you when a stream is active, enabling you to:
- Show a loading indicator
- Disable input fields during processing
- Display a cancel button
```tsx
export default function App() {
const { isLoading, stop } = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<form>
{isLoading && (
<button key="stop" type="button" onClick={() => stop()}>
Stop
</button>
)}
</form>
);
}
```
### Thread Management
Keep track of conversations with built-in thread management. You can access the current thread ID and get notified when new threads are created:
```tsx
const [threadId, setThreadId] = useState<string | null>(null);
const thread = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
threadId: threadId,
onThreadId: setThreadId,
});
```
We recommend storing the `threadId` in your URL's query parameters to let users resume conversations after page refreshes.
### Messages Handling
To enable messages handling, you need to pass the `messagesKey` option to the `useStream()` hook.
When enabled, the `useStream()` hook will keep track of the message chunks received from the server and concatenate them together to form a complete message. The completed message chunks can be retrieved via the `messages` property.
```tsx
import type { Message } from "@langchain/langgraph-sdk";
import { useStream } from "@langchain/langgraph-sdk/react";
export default function HomePage() {
const thread = useStream<{ messages: Message[] }>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<div>
{thread.messages.map((message) => (
<div key={message.id}>{message.content as string}</div>
))}
</div>
);
}
```
### Branching Support
To enable branching, you need to enable messages handling. Pass the `messagesKey` option to the `useStream()` hook. For each message, you can use `getMessagesMetadata()` to get the first checkpoint from which the message has been first seen. You can then create a new run from the checkpoint preceding the first seen checkpoint to create a new branch in a thread.
A branch can be created in following ways:
1. Edit a previous user message.
2. Request a regeneration of a previous assistant message.
```tsx
/* eslint-disable @typescript-eslint/no-floating-promises */
"use client";
import type { Message } from "@langchain/langgraph-sdk";
import { useStream } from "@langchain/langgraph-sdk/react";
import {
Annotation,
MessagesAnnotation,
type StateType,
type UpdateType,
} from "@langchain/langgraph/web";
import { useState } from "react";
const AgentState = Annotation.Root({
...MessagesAnnotation.spec,
});
function BranchSwitcher({
branch,
branchOptions,
onSelect,
}: {
branch: string | undefined;
branchOptions: string[] | undefined;
onSelect: (branch: string) => void;
}) {
if (!branchOptions || !branch) return null;
const index = branchOptions.indexOf(branch);
return (
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
const prevBranch = branchOptions[index - 1];
if (!prevBranch) return;
onSelect(prevBranch);
}}
>
Prev
</button>
<span>
{index + 1} / {branchOptions.length}
</span>
<button
type="button"
onClick={() => {
const nextBranch = branchOptions[index + 1];
if (!nextBranch) return;
onSelect(nextBranch);
}}
>
Next
</button>
</div>
);
}
function EditMessage({
message,
onEdit,
}: {
message: Message;
onEdit: (message: Message) => void;
}) {
const [editing, setEditing] = useState(false);
if (!editing) {
return (
<button type="button" onClick={() => setEditing(true)}>
Edit
</button>
);
}
return (
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const content = new FormData(form).get("content") as string;
form.reset();
onEdit({ type: "human", content });
setEditing(false);
}}
>
<input name="content" defaultValue={message.content as string} />
<button type="submit">Save</button>
</form>
);
}
export default function App() {
const thread = useStream<
StateType<typeof AgentState.spec>,
UpdateType<typeof AgentState.spec>
>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
return (
<div>
<div>
{thread.messages.map((message) => {
const meta = thread.getMessagesMetadata(message);
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
return (
<div key={message.id}>
<div>{message.content as string}</div>
{message.type === "human" && (
<EditMessage
message={message}
onEdit={(message) =>
thread.submit(
{ messages: [message] },
{ checkpoint: parentCheckpoint }
)
}
/>
)}
{message.type === "ai" && (
<button
type="button"
onClick={() =>
thread.submit(undefined, { checkpoint: parentCheckpoint })
}
>
<span>Regenerate</span>
</button>
)}
<BranchSwitcher
branch={meta?.branch}
branchOptions={meta?.branchOptions}
onSelect={(branch) => thread.setBranch(branch)}
/>
</div>
);
})}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const message = new FormData(form).get("message") as string;
form.reset();
thread.submit({ messages: [message] });
}}
>
<input type="text" name="message" />
{thread.isLoading ? (
<button key="stop" type="button" onClick={() => thread.stop()}>
Stop
</button>
) : (
<button key="submit" type="submit">
Send
</button>
)}
</form>
</div>
);
}
```
### TypeScript
The `useStream()` hook is fully typed to help catch errors early and provide better IDE support. You can specify types for:
- State shape
- Update format
- Custom events
```tsx
// Define your types
type State = {
messages: Message[];
context?: Record<string, unknown>;
};
type Update = {
messages: Message[] | Message;
context?: Record<string, unknown>;
};
type CustomEvent = {
type: "progress" | "debug";
payload: unknown;
};
// Use them with the hook
const thread = useStream<State, Update, CustomEvent>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
```
If you're using LangGraph.js, you can reuse your graph's annotation types:
```tsx
import {
Annotation,
MessagesAnnotation,
type StateType,
type UpdateType,
} from "@langchain/langgraph/web";
const AgentState = Annotation.Root({
...MessagesAnnotation.spec,
context: Annotation.Optional(Annotation.Any()),
});
const thread = useStream<
StateType<typeof AgentState.spec>,
UpdateType<typeof AgentState.spec>
>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
messagesKey: "messages",
});
```
## Event Handling
The `useStream()` hook provides several callback options to help you respond to different events:
- `onError`: Called when an error occurs.
- `onFinish`: Called when the stream is finished.
- `onUpdateEvent`: Called when an update event is received.
- `onCustomEvent`: Called when a custom event is received. See [Custom events](../../concepts/streaming.md#custom) to learn how to stream custom events.
- `onMetadataEvent`: Called when a metadata event is received.
## Learn More
- [JS/TS SDK Reference](../reference/sdk/js_ts_sdk_ref.md)
+7
View File
@@ -27,12 +27,19 @@ LangGraph Platform provides different security defaults:
- Requires valid API key in `x-api-key` header
- Can be customized with your auth handler
!!! note "Custom auth"
Custom auth **is supported** for all plans in LangGraph Cloud.
### Self-Hosted
- No default authentication
- Complete flexibility to implement your security model
- You control all aspects of authentication and authorization
!!! note "Custom auth"
Custom auth is supported for **Enterprise** self-hosted plans.
Self-hosted lite plans do not support custom auth natively.
## System Architecture
A typical authentication setup involves three main components:
+1 -1
View File
@@ -88,7 +88,7 @@ We recommend that you [**use the `interrupt` function instead**](#the-interrupt-
??? node "`NodeInterrupt` exception"
The developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of [dynamic breakpoints](./low_level.md#dynamic-breakpoints) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
The developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of _dynamic breakpoints_ is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
```python
def my_node(state: State) -> State:
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

+5 -6
View File
@@ -26,8 +26,8 @@ The conceptual guide does not cover step-by-step instructions or specific implem
- [Human-in-the-Loop](human_in_the_loop.md): Explains different ways of integrating human feedback into a LangGraph application.
- [Time Travel](time-travel.md): Time travel allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues.
- [Persistence](persistence.md): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [Functional API (beta)](functional_api.md): An alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
- [FAQ](faq.md): Frequently asked questions about LangGraph.
@@ -37,7 +37,6 @@ LangGraph Platform is a commercial solution for deploying agentic applications i
The LangGraph Platform offers a few different deployment options described in the [deployment options guide](./deployment_options.md).
!!! tip
* LangGraph is an MIT-licensed open-source library, which we are committed to maintaining and growing for the community.
@@ -46,6 +45,7 @@ The LangGraph Platform offers a few different deployment options described in th
### High Level
- [Why LangGraph Platform?](./langgraph_platform.md): The LangGraph platform is an opinionated way to deploy and manage LangGraph applications. This guide provides an overview of the key features and concepts behind LangGraph Platform.
- [Platform Architecture](./platform_architecture.md): A high-level overview of the architecture of the LangGraph Platform.
- [Deployment Options](./deployment_options.md): LangGraph Platform offers four deployment options: [Self-Hosted Lite](./self_hosted.md#self-hosted-lite), [Self-Hosted Enterprise](./self_hosted.md#self-hosted-enterprise), [bring your own cloud (BYOC)](./bring_your_own_cloud.md), and [Cloud SaaS](./langgraph_cloud.md). This guide explains the differences between these options, and which Plans they are available on.
- [Plans](./plans.md): LangGraph Platforms offer three different plans: Developer, Plus, Enterprise. This guide explains the differences between these options, what deployment options are available for each, and how to sign up for each one.
- [Template Applications](./template_applications.md): Reference applications designed to help you get started quickly when building with LangGraph.
@@ -54,7 +54,7 @@ The LangGraph Platform offers a few different deployment options described in th
The LangGraph Platform comprises several components that work together to support the deployment and management of LangGraph applications:
- [LangGraph Server](./langgraph_server.md): The LangGraph Server is designed to support a wide range of agentic application use cases, from background processing to real-time interactions.
- [LangGraph Server](./langgraph_server.md): The LangGraph Server is designed to support a wide range of agentic application use cases, from background processing to real-time interactions.
- [LangGraph Studio](./langgraph_studio.md): LangGraph Studio is a specialized IDE that can connect to a LangGraph Server to enable visualization, interaction, and debugging of the application locally.
- [LangGraph CLI](./langgraph_cli.md): LangGraph CLI is a command-line interface that helps to interact with a local LangGraph
- [Python/JS SDK](./sdk.md): The Python/JS SDK provides a programmatic way to interact with deployed LangGraph Applications.
@@ -71,8 +71,7 @@ The LangGraph Platform comprises several components that work together to suppor
### Deployment Options
- [Self-Hosted Lite](./self_hosted.md): A free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner
- [Cloud SaaS](./langgraph_cloud.md): Hosted as part of LangSmith.
- [Bring Your Own Cloud](./bring_your_own_cloud.md): We manage the infrastructure, so you don't have to, but the infrastructure all runs within your cloud.
- [Self-Hosted Enterprise](./self_hosted.md): Completely managed by you.
- [Self-Hosted Enterprise](./self_hosted.md): Completely managed by you.
+10 -10
View File
@@ -213,9 +213,9 @@ builder.add_node("other_node", my_other_node)
...
```
Behind the scenes, functions are converted to [RunnableLambda's](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda), which add batch and async support to your function, along with native tracing and debugging.
Behind the scenes, functions are converted to [RunnableLambda](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda)s, which add batch and async support to your function, along with native tracing and debugging.
If you add a node to graph without specifying a name, it will be given a default name equivalent to the function name.
If you add a node to a graph without specifying a name, it will be given a default name equivalent to the function name.
```python
builder.add_node(my_node)
@@ -224,7 +224,7 @@ builder.add_node(my_node)
### `START` Node
The `START` Node is a special node that represents the node sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.
The `START` Node is a special node that represents the node that sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.
```python
from langgraph.graph import START
@@ -269,9 +269,9 @@ If you want to **optionally** route to 1 or more edges (or optionally terminate)
graph.add_conditional_edges("node_a", routing_function)
```
Similar to nodes, the `routing_function` accept the current `state` of the graph and return a value.
Similar to nodes, the `routing_function` accepts the current `state` of the graph and returns a value.
By default, the return value `routing_function` is used as the name of the node (or a list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep.
By default, the return value `routing_function` is used as the name of the node (or list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep.
You can optionally provide a dictionary that maps the `routing_function`'s output to the name of the next node.
@@ -310,7 +310,7 @@ graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "no
## `Send`
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common of example of this is with `map-reduce` design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common example of this is with `map-reduce` design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
To support this design pattern, LangGraph supports returning [`Send`][langgraph.types.Send] objects from conditional edges. `Send` takes two arguments: first is the name of the node, and second is the state to pass to that node.
@@ -357,7 +357,7 @@ Use [conditional edges](#conditional-edges) to route between nodes conditionally
### Navigating to a node in a parent graph
If you are using [subgraphs](#subgraphs), you might want to navigate from a node a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
If you are using [subgraphs](#subgraphs), you might want to navigate from a node within a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
```python
def my_node(state: State) -> Command[Literal["my_other_node"]]:
@@ -400,7 +400,7 @@ def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: R
!!! important
You MUST include `messages` (or any state key used for the message history) in `Command.update` when returning `Command` from a tool and the list of messages in `messages` MUST contain a `ToolMessage`. This is necessary for the resulting message history to be valid (LLM providers require AI messages with tool calls to be followed by the tool result messages).
If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from node.
If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from the node.
### Human-in-the-loop
@@ -494,7 +494,7 @@ Read more about how the `interrupt` is used for **human-in-the-loop** workflows
## Breakpoints
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](#interrupt-function) for this purpose.
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](#interrupt) for this purpose.
Read more about breakpoints in the [Breakpoints conceptual guide](./breakpoints.md).
@@ -531,7 +531,7 @@ Let's take a look at examples for each.
### As a compiled graph
The simplest way to create subgraph nodes is by using a [compiled subgraph](#compiling-your-graph) directly. When doing so, it is **important** that the parent graph and the subgraph [state schemas](#state) share at least one key which they can use to communicate. If your graph and subgraph do not share any keys, you should use write a function [invoking the subgraph](#as-a-function) instead.
The simplest way to create subgraph nodes is by using a [compiled subgraph](#compiling-your-graph) directly. When doing so, it is **important** that the parent graph and the subgraph [state schemas](#state) share at least one key which they can use to communicate. If your graph and subgraph do not share any keys, you should write a function [invoking the subgraph](#as-a-function) instead.
!!! Note
If you pass extra keys to the subgraph node (i.e., in addition to the shared keys), they will be ignored by the subgraph node. Similarly, if you return extra keys from the subgraph, they will be ignored by the parent graph.
@@ -0,0 +1,23 @@
# LangGraph Platform Architecture
![](img/langgraph_platform_deployment_architecture.png)
## How we use Postgres
Postgres is the persistence layer for all user and run data in LGP. This stores both checkpoints (see more info [here](./persistence.md)) as well as the server resources (threads, runs, assistants and crons).
## How we use Redis
Redis is used in each LGP deployment as a way for server and queue workers to communicate, and to store ephemeral metadata, more details on both below. No user/run data is stored in Redis.
### Communication
All runs in LGP are executed by the pool of background workers that are part of each deployment. In order to enable some features for those runs (such as cancellation and output streaming) we need a channel for two-way communication between the server and the worker handling a particular run. We use Redis to organize that communication.
1. A Redis list is used as a mechanism to wake up a worker as soon as a new run is created. Only a sentinel value is stored in this list, no actual run info. The run information is then retrieved from Postgres by the worker.
2. A combination of a Redis string and Redis PubSub channel is used for the server to communicate a run cancellation request to the appropriate worker.
3. A Redis PubSub channel is used by the worker to broadcast streaming output from an agent while the run is being handled. Any open `/stream` request in the server will subscribe to that channel and forward any events to the response as they arrive. No events are stored in Redis at any time.
### Ephemeral metadata
Runs in an LGP deployment may be retried for specific failures (currently only for transient Postgres errors encountered during the run). In order to limit the number of retries (currently limited to 3 attempts per run) we record the attempt number in a Redis string when is picked up. This contains no run-specific info other than its ID, and expires after a short delay.
+8 -119
View File
@@ -1,6 +1,12 @@
# Streaming
LangGraph is built with first class support for streaming. There are several different ways to stream back outputs from a graph run
Building a responsive app for end-users? Real-time updates are key to keeping users engaged as your app progresses.
There are three main types of data youll want to stream:
1. Workflow progress (e.g., get state updates after each graph node is executed).
2. LLM tokens as theyre generated.
3. Custom updates (e.g., "Fetched 10/100 records").
## Streaming graph outputs (`.stream` and `.astream`)
@@ -31,123 +37,6 @@ The below visualization shows the difference between the `values` and `updates`
![values vs updates](../static/values_vs_updates.png)
## Streaming LLM tokens and events (`.astream_events`)
In addition, you can use the `astream_events` method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb).
This is a standard method on all [LangChain objects](https://python.langchain.com/docs/concepts/#runnable-interface). This means that as the graph is executed, certain events are emitted along the way and can be seen if you run the graph using `.astream_events`.
All events have (among other things) `event`, `name`, and `data` fields. What do these mean?
- `event`: This is the type of event that is being emitted. You can find a detailed table of all callback events and triggers [here](https://python.langchain.com/docs/concepts/#callback-events).
- `name`: This is the name of event.
- `data`: This is the data associated with the event.
What types of things cause events to be emitted?
* each node (runnable) emits `on_chain_start` when it starts execution, `on_chain_stream` during the node execution and `on_chain_end` when the node finishes. Node events will have the node name in the event's `name` field
* the graph will emit `on_chain_start` in the beginning of the graph execution, `on_chain_stream` after each node execution and `on_chain_end` when the graph finishes. Graph events will have the `LangGraph` in the event's `name` field
* Any writes to state channels (i.e. anytime you update the value of one of your state keys) will emit `on_chain_start` and `on_chain_end` events
Additionally, any events that are created inside your nodes (LLM events, tool events, manually emitted events, etc.) will also be visible in the output of `.astream_events`.
To make this more concrete and to see what this looks like, let's see what events are returned when we run a simple graph:
```python
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START, END
model = ChatOpenAI(model="gpt-4o-mini")
def call_model(state: MessagesState):
response = model.invoke(state['messages'])
return {"messages": response}
workflow = StateGraph(MessagesState)
workflow.add_node(call_model)
workflow.add_edge(START, "call_model")
workflow.add_edge("call_model", END)
app = workflow.compile()
inputs = [{"role": "user", "content": "hi!"}]
async for event in app.astream_events({"messages": inputs}, version="v1"):
kind = event["event"]
print(f"{kind}: {event['name']}")
```
```shell
on_chain_start: LangGraph
on_chain_start: __start__
on_chain_end: __start__
on_chain_start: call_model
on_chat_model_start: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_stream: ChatOpenAI
on_chat_model_end: ChatOpenAI
on_chain_start: ChannelWrite<call_model,messages>
on_chain_end: ChannelWrite<call_model,messages>
on_chain_stream: call_model
on_chain_end: call_model
on_chain_stream: LangGraph
on_chain_end: LangGraph
```
We start with the overall graph start (`on_chain_start: LangGraph`). We then write to the `__start__` node (this is special node to handle input).
We then start the `call_model` node (`on_chain_start: call_model`). We then start the chat model invocation (`on_chat_model_start: ChatOpenAI`),
stream back token by token (`on_chat_model_stream: ChatOpenAI`) and then finish the chat model (`on_chat_model_end: ChatOpenAI`). From there,
we write the results back to the channel (`ChannelWrite<call_model,messages>`) and then finish the `call_model` node and then the graph as a whole.
This should hopefully give you a good sense of what events are emitted in a simple graph. But what data do these events contain?
Each type of event contains data in a different format. Let's look at what `on_chat_model_stream` events look like. This is an important type of event
since it is needed for streaming tokens from an LLM response.
These events look like:
```shell
{'event': 'on_chat_model_stream',
'name': 'ChatOpenAI',
'run_id': '3fdbf494-acce-402e-9b50-4eab46403859',
'tags': ['seq:step:1'],
'metadata': {'langgraph_step': 1,
'langgraph_node': 'call_model',
'langgraph_triggers': ['start:call_model'],
'langgraph_task_idx': 0,
'checkpoint_id': '1ef657a0-0f9d-61b8-bffe-0c39e4f9ad6c',
'checkpoint_ns': 'call_model',
'ls_provider': 'openai',
'ls_model_name': 'gpt-4o-mini',
'ls_model_type': 'chat',
'ls_temperature': 0.7},
'data': {'chunk': AIMessageChunk(content='Hello', id='run-3fdbf494-acce-402e-9b50-4eab46403859')},
'parent_ids': []}
```
We can see that we have the event type and name (which we knew from before).
We also have a bunch of stuff in metadata. Noticeably, `'langgraph_node': 'call_model',` is some really helpful information
which tells us which node this model was invoked inside of.
Finally, `data` is a really important field. This contains the actual data for this event! Which in this case
is an AIMessageChunk. This contains the `content` for the message, as well as an `id`.
This is the ID of the overall AIMessage (not just this chunk) and is super helpful - it helps
us track which chunks are part of the same message (so we can show them together in the UI).
This information contains all that is needed for creating a UI for streaming LLM tokens. You can see a
guide for that [here](../how-tos/streaming-tokens.ipynb).
!!! warning "ASYNC IN PYTHON<=3.10"
You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case.
## LangGraph Platform
Streaming is critical for making LLM applications feel responsive to end users. When creating a streaming run, the streaming mode determines what data is streamed back to the API client. LangGraph Platform supports five streaming modes:
@@ -155,8 +44,8 @@ Streaming is critical for making LLM applications feel responsive to end users.
- `values`: Stream the full state of the graph after each [super-step](https://langchain-ai.github.io/langgraph/concepts/low_level/#graphs) is executed. See the [how-to guide](../cloud/how-tos/stream_values.md) for streaming values.
- `messages-tuple`: Stream LLM tokens for any messages generated inside a node. This mode is primarily meant for powering chat applications. See the [how-to guide](../cloud/how-tos/stream_messages.md) for streaming messages.
- `updates`: Streams updates to the state of the graph after each node is executed. See the [how-to guide](../cloud/how-tos/stream_updates.md) for streaming updates.
- `events`: Stream all events (including the state of the graph) that occur during graph execution. See the [how-to guide](../cloud/how-tos/stream_events.md) for streaming events. This can be used to do token-by-token streaming for LLMs.
- `debug`: Stream debug events throughout graph execution. See the [how-to guide](../cloud/how-tos/stream_debug.md) for streaming debug events.
- `events`: Stream all events (including the state of the graph) that occur during graph execution. See the [how-to guide](../cloud/how-tos/stream_events.md) for streaming events. This mode is only useful for users migrating large LCEL applications to LangGraph. Generally, this mode is not necessary for most applications.
You can also specify multiple streaming modes at the same time. See the [how-to guide](../cloud/how-tos/stream_multiple.md) for configuring multiple streaming modes at the same time.
+1 -1
View File
@@ -58,7 +58,7 @@
"\n",
"This guide shows how you can:\n",
"\n",
"- implement handoffs using `Command`: agent node makes some decision (usually LLM-based), and explicitly returns a handoff via `Command`. These are useful when you need fine-grained control over how an agent routes to another agent. It could be well suited for implementing a supervisor agent in a supervisor architecture.\n",
"- implement handoffs using `Command`: agent node makes a decision on who to hand off to (usually LLM-based), and explicitly returns a handoff via `Command`. These are useful when you need fine-grained control over how an agent routes to another agent. It could be well suited for implementing a supervisor agent in a supervisor architecture.\n",
"- implement handoffs using tools: a tool-calling agent has access to tools that can return a handoff via `Command`. The tool-executing node in the agent recognizes `Command` objects returned by the tools and routes accordingly. Handoff tool a general-purpose primitive that is useful in any multi-agent systems that contain tool-calling agents."
]
},
+4 -2
View File
@@ -13,12 +13,14 @@
We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon.
???+ note "Support by deployment type"
Custom auth is supported for all deployments in the **managed LangGraph Cloud**, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
This guide shows how to add custom authentication to your LangGraph Platform application. This guide applies to both LangGraph Cloud, BYOC, and self-hosted deployments. It does not apply to isolated usage of the LangGraph open source library in your own custom server.
## 1. Implement authentication
Create `auth.py` file, with a basic JWT authentication handler:
```python
from langgraph_sdk import Auth
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -47,7 +47,7 @@ First let's install the required packages and set our API keys
```
```python exec="on" source="above" session="1"
```python
import getpass
import os
+4 -3
View File
@@ -11,9 +11,9 @@ Here youll find answers to “How do I...?” types of questions. These guide
### Graph API Basics
- [How to update graph state from nodes](state-reducers.ipynb)
- [How to create a sequence of steps](sequence.ipynb)
- [How to create branches for parallel execution](branching.ipynb)
- [How to update graph state from nodes](state-reducers.md)
- [How to create a sequence of steps](sequence.md)
- [How to create branches for parallel execution](branching.md)
- [How to create and control loops with recursion limits](recursion-limit.ipynb)
- [How to visualize your graph](visualization.ipynb)
@@ -204,6 +204,7 @@ Learn how to set up your app for deployment to LangGraph Platform:
- [How to test locally](../cloud/deployment/test_locally.md)
- [How to rebuild graph at runtime](../cloud/deployment/graph_rebuild.md)
- [How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks](autogen-langgraph-platform.ipynb)
- [How to integrate LangGraph into your React application](../cloud/how-tos/use_stream_react.md)
### Deployment
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -12,8 +12,9 @@ below. These libraries can extend LangGraph's functionality in various ways.
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
| Name | GitHub URL | Description | Weekly Downloads |
| --- | --- | --- | --- |
| **trustcall** | [hinthornw/trustcall](https://github.com/hinthornw/trustcall) | Tenacious tool calling built on LangGraph | 6976 |
| **langgraph-supervisor** | [langchain-ai/langgraph-supervisor](https://github.com/langchain-ai/langgraph-supervisor) | Build supervisor multi-agent systems with LangGraph | 421 |
| **trustcall** | [hinthornw/trustcall](https://github.com/hinthornw/trustcall) | Tenacious tool calling built on LangGraph | 8803 |
| **langgraph-supervisor** | [langchain-ai/langgraph-supervisor](https://github.com/langchain-ai/langgraph-supervisor) | Build supervisor multi-agent systems with LangGraph | 636 |
| **breeze-agent** | [andrestorres123/breeze-agent](https://github.com/andrestorres123/breeze-agent) | A streamlined research system built inspired on STORM and built on LangGraph | 184 |
## ✨ Contributing Your Library
@@ -17,6 +17,11 @@
We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon.
???+ note "Support by deployment type"
Custom auth is supported for all deployments in the **managed LangGraph Cloud**, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
In this tutorial, we will build a chatbot that only lets specific users access it. We'll start with the LangGraph template and add token-based security step by step. By the end, you'll have a working chatbot that checks for valid tokens before allowing access.
## Setting up our project
+19 -6
View File
@@ -123,9 +123,9 @@ nav:
- LangGraph: how-tos#langgraph
- Graph API Basics:
- Graph API Basics: how-tos#graph-api-basics
- how-tos/state-reducers.ipynb
- how-tos/sequence.ipynb
- how-tos/branching.ipynb
- how-tos/state-reducers.md
- how-tos/sequence.md
- how-tos/branching.md
- how-tos/recursion-limit.ipynb
- how-tos/visualization.ipynb
- Controllability:
@@ -253,6 +253,7 @@ nav:
- cloud/how-tos/stream_events.md
- cloud/how-tos/stream_debug.md
- cloud/how-tos/stream_multiple.md
- cloud/how-tos/use_stream_react.md
- Human-in-the-loop:
- Human-in-the-loop: how-tos#human-in-the-loop_1
- cloud/how-tos/human_in_the_loop_breakpoint.md
@@ -467,6 +468,16 @@ markdown_extensions:
hooks:
- _scripts/notebook_hooks.py
extra:
consent:
title: Cookie consent
actions:
- accept
- reject
description: >-
We use cookies to recognize your repeated visits and preferences, as well
as to measure the effectiveness of our documentation and whether users
find what they're searching for. <strong>Clicking "Accept" makes our
documentation better. Thank you!</strong> ❤️
social:
- icon: fontawesome/brands/js
link: https://langchain-ai.github.io/langgraphjs/
@@ -475,9 +486,9 @@ extra:
- icon: fontawesome/brands/twitter
link: https://twitter.com/LangChainAI
analytics:
- provider: google
- property: G-G8X6ELZYE0
- feedback:
provider: google
property: G-WR87FQLG9F
feedback:
title: Was this page helpful?
ratings:
- icon: material/emoticon-happy-outline
@@ -508,3 +519,5 @@ validation:
anchors: info
# this is needed to handle headers with anchors for nav
not_found: info
copyright: >
Copyright &copy; 2025 LangChain, Inc | <a href="#__consent">Consent Preferences</a>
+908 -7
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
aiohappyeyeballs = "2.4.3"
pygments-ansi-color = ">=0.3"
hub = "^3.0.1"
[tool.poetry.group.docs.dependencies]
langgraph = { path = "../libs/langgraph/", develop = true }
@@ -17,6 +17,7 @@ langgraph-checkpoint = { path = "../libs/checkpoint/", develop = true }
langgraph-checkpoint-sqlite = { path = "../libs/checkpoint-sqlite", develop = true }
langgraph-checkpoint-postgres = { path = "../libs/checkpoint-postgres", develop = true }
langgraph-sdk = {path = "../libs/sdk-py", develop = true}
langchain-ollama = "^0.2.3"
mkdocs = "^1.6.0"
mkdocs-autorefs = ">=1.0.1,<1.1.0"
mkdocstrings = "^0.25.1"
@@ -28,10 +29,14 @@ mkdocs-material = {extras = ["imaging"], version = "^9.5.27"}
markdown-callouts = "^0.4.0"
markdown-include = "^0.8.1"
mkdocs-exclude = "^1.0.2"
psycopg = {extras = ["binary"], version = "^3.2.0"}
psycopg-pool = "^3.2.0"
pygments-ansi-color = ">=0.3"
vcrpy = "^6.0.1"
click = "^8.1.7"
ruff = "^0.6.8"
jupyter = "^1.1.1"
langchain-cohere = "^0.4.2"
[tool.poetry.group.test.dependencies]
langchain = "^0.3.8"
@@ -41,6 +46,7 @@ langchain-nomic = "^0.1.3"
langchain-fireworks = "^0.2.0"
langchain-community = "^0.3.0"
langchain-experimental = "^0.3.2"
langchain-mistralai = "^0.2.6"
langgraph-checkpoint-mongodb = "^0.1.0"
langsmith = "^0.2.0"
chromadb = "^0.5.5"
View File
View File
+130
View File
@@ -0,0 +1,130 @@
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.structure.files import File
from mkdocs.structure.pages import Page
from _scripts.notebook_hooks import _highlight_code_blocks, on_page_markdown
NO_OP_INPUT_1 = """\
This is a plain text without any code blocks.
```python
print("Hello, World!")
```
"""
NO_OP_INPUT_2 = """\
=== "Python"
```python
def foo():
pass
print("Hello, World!")
```
"""
def test_highlight_code_blocks_no_op() -> None:
assert _highlight_code_blocks(NO_OP_INPUT_1) == NO_OP_INPUT_1
assert _highlight_code_blocks(NO_OP_INPUT_2) == NO_OP_INPUT_2
# Examples are written in multiline style to make sure that whitespace
# is easy to interpret.
INPUT_HIGHLIGHT_1 = """\
This is a plain text without any code blocks.
```python
# highlight-next-line
print("Hello, World!")
```
"""
EXPECTED_HIGHLIGHT_1 = """\
This is a plain text without any code blocks.
```python hl_lines="1"
print("Hello, World!")
```
"""
INPUT_HIGHLIGHT_2 = """\
This is a plain text without any code blocks.
```python
# highlight-next-line
print("Hello, World!")
x = 5
# highlight-next-line
print("Hello, World!")
```
"""
EXPECTED_HIGHLIGHT_2 = """\
This is a plain text without any code blocks.
```python hl_lines="1 5"
print("Hello, World!")
x = 5
print("Hello, World!")
```
"""
# Test end-to-end behavior of on_page_markdown
INPUT_HIGHLIGHT_3 = """\
```python exec="on" source="below"
print("Hello, World!")
# highlight-next-line
print("Hello, World!")
```
"""
EXPECTED_HIGHLIGHT_3 = """\
```python exec="on" source="below" hl_lines="2"
print("Hello, World!")
print("Hello, World!")
```
"""
def test_highlight_code_blocks() -> None:
"""Test that code blocks are highlighted correctly."""
assert _highlight_code_blocks(INPUT_HIGHLIGHT_1) == EXPECTED_HIGHLIGHT_1
assert _highlight_code_blocks(INPUT_HIGHLIGHT_2) == EXPECTED_HIGHLIGHT_2
assert _highlight_code_blocks(INPUT_HIGHLIGHT_3) == EXPECTED_HIGHLIGHT_3
END_TO_END_INPUT_HIGHLIGHT_1 = """\
```python exec="on" source="below"
print("Hello, World!")
# highlight-next-line
print("Hello, World!")
```
"""
END_TO_END_INPUT_HIGHLIGHT_1_EXPECT = """\
```python exec="on" source="below" hl_lines="2" path="dummy.md"
print("Hello, World!")
print("Hello, World!")
```
"""
def test_on_page_markdown_highlights() -> None:
"""Test that on page markdown behaves correctly."""
# Create a dummy MkDocs File and Page object.
dummy_file = File("dummy.md", "dummy.md", "placeholder", use_directory_urls=False)
dummy_page = Page("Test Page", dummy_file, config=MkDocsConfig())
assert (
on_page_markdown(END_TO_END_INPUT_HIGHLIGHT_1, dummy_page)
== END_TO_END_INPUT_HIGHLIGHT_1_EXPECT
)
@@ -0,0 +1,105 @@
import nbformat
import pytest
from _scripts.notebook_convert import (
_convert_links_in_markdown,
md_executable,
_has_output,
)
EXPECTED_OUTPUT = """\
```python exec="on" source="above" session="1" result="ansi"
print("Hello, world!")
```
"""
def test_convert_normal_code_block() -> None:
notebook = nbformat.v4.new_notebook()
notebook.metadata.language_info = {"name": "python", "version": "3.11"}
notebook.cells.append(nbformat.v4.new_code_cell('print("Hello, world!")'))
markdown, _ = md_executable.from_notebook_node(notebook)
assert markdown == EXPECTED_OUTPUT
# We treat cell magic as a non-executable code block.
CELL_MAGIC_INPUT = """\
%%capture
%pip install numpy
"""
CELL_MAGIC_OUTPUT = """\
```shell
pip install numpy
```
"""
def test_convert_cell_magic() -> None:
notebook = nbformat.v4.new_notebook()
notebook.metadata.language_info = {"name": "python", "version": "3.11"}
notebook.cells.append(nbformat.v4.new_code_cell(CELL_MAGIC_INPUT))
markdown, _ = md_executable.from_notebook_node(notebook)
assert markdown == CELL_MAGIC_OUTPUT
STDIN_INPUT = """\
input("Enter your name: ")\
"""
STDIN_OUTPUT = """\
```python
input("Enter your name: ")
```
"""
def test_convert_input_cell() -> None:
notebook = nbformat.v4.new_notebook()
notebook.metadata.language_info = {"name": "python", "version": "3.11"}
notebook.cells.append(nbformat.v4.new_code_cell(STDIN_INPUT))
markdown, _ = md_executable.from_notebook_node(notebook)
assert markdown == STDIN_OUTPUT
NO_STDOUT_EXPECTED = """\
```python exec="on" source="above" session="1"
display(x)
```
"""
def test_convert_block_without_output() -> None:
notebook = nbformat.v4.new_notebook()
notebook.metadata.language_info = {"name": "python", "version": "3.11"}
notebook.cells.append(nbformat.v4.new_code_cell("display(x)"))
markdown, _ = md_executable.from_notebook_node(notebook)
assert markdown == NO_STDOUT_EXPECTED
def test_has_output() -> None:
"""Test if a given code block is expected to have output."""
assert _has_output("print('Hello, world!')") is True
assert _has_output("print_stream(some_iterable)") is True
assert _has_output("foo.y") is True
assert _has_output("display(x)") is False
assert _has_output("assert 1 == 1") is False
assert _has_output("def foo(): pass") is False
assert _has_output("import foobar") is False
@pytest.mark.parametrize(
"source, expected",
[
(
"This is a [link](https://example.com).",
"This is a [link](https://example.com).",
),
("This is a [link](../foo).", "This is a [link](foo.md)."),
("This is a [link](../foo#hello).", "This is a [link](foo.md#hello)."),
("This is a [link](../foo/#hello).", "This is a [link](foo.md#hello)."),
],
)
def test_link_conversion(source: str, expected: str) -> None:
"""Test logic to convert links in markdown cells."""
assert _convert_links_in_markdown(source) == expected
@@ -16,6 +16,7 @@ from langgraph.checkpoint.base import (
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_id,
get_checkpoint_metadata,
)
from langgraph.checkpoint.postgres import _internal
from langgraph.checkpoint.postgres.base import BasePostgresSaver
@@ -317,17 +318,7 @@ class PostgresSaver(BasePostgresSaver):
checkpoint["id"],
checkpoint_id,
Jsonb(self._dump_checkpoint(copy)),
self._dump_metadata(
{
**{
k: v
for k, v in config["configurable"].items()
if not k.startswith("__")
},
**config.get("metadata", {}),
**metadata,
}
),
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
),
)
return next_config
@@ -16,6 +16,7 @@ from langgraph.checkpoint.base import (
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_id,
get_checkpoint_metadata,
)
from langgraph.checkpoint.postgres import _ainternal
from langgraph.checkpoint.postgres.base import BasePostgresSaver
@@ -275,17 +276,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
checkpoint["id"],
checkpoint_id,
Jsonb(self._dump_checkpoint(copy)),
self._dump_metadata(
{
**{
k: v
for k, v in config["configurable"].items()
if not k.startswith("__")
},
**config.get("metadata", {}),
**metadata,
}
),
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
),
)
return next_config
@@ -24,6 +24,7 @@ from langgraph.checkpoint.base import (
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_metadata,
)
from langgraph.checkpoint.postgres import _ainternal, _internal
from langgraph.checkpoint.postgres.base import BasePostgresSaver
@@ -423,17 +424,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
thread_id,
checkpoint_ns,
Jsonb(self._dump_checkpoint(copy)),
self._dump_metadata(
{
**{
k: v
for k, v in config["configurable"].items()
if not k.startswith("__")
},
**config.get("metadata", {}),
**metadata,
}
),
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
),
)
return next_config
@@ -752,17 +743,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
thread_id,
checkpoint_ns,
Jsonb(self._dump_checkpoint(copy)),
self._dump_metadata(
{
**{
k: v
for k, v in config["configurable"].items()
if not k.startswith("__")
},
**config.get("metadata", {}),
**metadata,
}
),
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
),
)
return next_config
+11 -22
View File
@@ -187,22 +187,12 @@ description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
markers = "sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "docopt"
version = "0.6.2"
description = "Pythonic argument parser, that will make you smile"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
@@ -358,7 +348,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.10"
version = "2.0.15"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -962,21 +952,20 @@ pytest = ">=6.2.5"
dev = ["pre-commit", "pytest-asyncio", "tox"]
[[package]]
name = "pytest-watch"
version = "4.2.0"
description = "Local continuous test runner with pytest and watchdog."
name = "pytest-watcher"
version = "0.4.3"
description = "Automatically rerun your tests on file modifications"
optional = false
python-versions = "*"
python-versions = "<4.0.0,>=3.7.0"
groups = ["dev"]
files = [
{file = "pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"},
{file = "pytest_watcher-0.4.3-py3-none-any.whl", hash = "sha256:d59b1e1396f33a65ea4949b713d6884637755d641646960056a90b267c3460f9"},
{file = "pytest_watcher-0.4.3.tar.gz", hash = "sha256:0cb0e4661648c8c0ff2b2d25efa5a8e421784b9e4c60fcecbf9b7c30b2d731b3"},
]
[package.dependencies]
colorama = ">=0.3.3"
docopt = ">=0.4.0"
pytest = ">=2.6.4"
watchdog = ">=0.6.0"
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
watchdog = ">=2.0.0"
[[package]]
name = "pyyaml"
@@ -1266,4 +1255,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.1"
python-versions = "^3.9.0,<4.0"
content-hash = "61326e4e81a4e8854763a119f39d4f5d0a54cee868b4dbc91b95ce7d2cebba5b"
content-hash = "369bfffecb9489835b43b8255932e043176a11d2f639aad2d055ffd89263ca1e"
+9 -3
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
version = "2.0.14"
version = "2.0.15"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
langgraph-checkpoint = "^2.0.10"
langgraph-checkpoint = "^2.0.15"
orjson = ">=3.10.1"
psycopg = "^3.2.0"
psycopg-pool = "^3.2.0"
@@ -22,10 +22,10 @@ pytest = "^7.2.1"
anyio = "^4.4.0"
pytest-asyncio = "^0.21.1"
pytest-mock = "^3.11.1"
pytest-watch = "^4.2.0"
mypy = "^1.10.0"
psycopg = {extras = ["binary"], version = ">=3.0.0"}
langgraph-checkpoint = {path = "../checkpoint", develop = true}
pytest-watcher = "^0.4.3"
[tool.pytest.ini_options]
# --strict-markers will raise errors on unknown marks.
@@ -61,3 +61,9 @@ warn_unused_ignores = "True"
warn_redundant_casts = "True"
allow_redefinition = "True"
disable_error_code = "typeddict-item, return-value"
[tool.pytest-watcher]
now = true
delay = 0.1
runner_args = ["--ff", "-x", "-v", "--tb", "short"]
patterns = ["*.py"]
+7 -3
View File
@@ -11,6 +11,7 @@ from psycopg.rows import dict_row
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.base import (
EXCLUDED_METADATA_KEYS,
Checkpoint,
CheckpointMetadata,
create_checkpoint,
@@ -23,6 +24,10 @@ from langgraph.checkpoint.postgres.aio import (
from tests.conftest import DEFAULT_POSTGRES_URI
def _exclude_keys(config: dict[str, Any]) -> dict[str, Any]:
return {k: v for k, v in config.items() if k not in EXCLUDED_METADATA_KEYS}
@asynccontextmanager
async def _pool_saver():
"""Fixture for pool mode testing."""
@@ -223,7 +228,6 @@ async def test_combined_metadata(saver_name: str, test_data) -> None:
assert checkpoint.metadata == {
**metadata,
"thread_id": "thread-2",
"checkpoint_ns": "",
"run_id": "my_run_id",
}
@@ -251,14 +255,14 @@ async def test_asearch(saver_name: str, test_data) -> None:
search_results_1 = [c async for c in saver.alist(None, filter=query_1)]
assert len(search_results_1) == 1
assert search_results_1[0].metadata == {
**configs[0]["configurable"],
**_exclude_keys(configs[0]["configurable"]),
**metadata[0],
}
search_results_2 = [c async for c in saver.alist(None, filter=query_2)]
assert len(search_results_2) == 1
assert search_results_2[0].metadata == {
**configs[1]["configurable"],
**_exclude_keys(configs[1]["configurable"]),
**metadata[1],
}
+7 -3
View File
@@ -12,6 +12,7 @@ from psycopg.rows import dict_row
from psycopg_pool import ConnectionPool
from langgraph.checkpoint.base import (
EXCLUDED_METADATA_KEYS,
Checkpoint,
CheckpointMetadata,
create_checkpoint,
@@ -21,6 +22,10 @@ from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
from tests.conftest import DEFAULT_POSTGRES_URI
def _exclude_keys(config: dict[str, Any]) -> dict[str, Any]:
return {k: v for k, v in config.items() if k not in EXCLUDED_METADATA_KEYS}
@contextmanager
def _pool_saver():
"""Fixture for pool mode testing."""
@@ -205,7 +210,6 @@ def test_combined_metadata(saver_name: str, test_data) -> None:
assert checkpoint.metadata == {
**metadata,
"thread_id": "thread-2",
"checkpoint_ns": "",
"run_id": "my_run_id",
}
@@ -233,14 +237,14 @@ def test_search(saver_name: str, test_data) -> None:
search_results_1 = list(saver.list(None, filter=query_1))
assert len(search_results_1) == 1
assert search_results_1[0].metadata == {
**configs[0]["configurable"],
**_exclude_keys(configs[0]["configurable"]),
**metadata[0],
}
search_results_2 = list(saver.list(None, filter=query_2))
assert len(search_results_2) == 1
assert search_results_2[0].metadata == {
**configs[1]["configurable"],
**_exclude_keys(configs[1]["configurable"]),
**metadata[1],
}
@@ -15,6 +15,7 @@ from langgraph.checkpoint.base import (
CheckpointTuple,
SerializerProtocol,
get_checkpoint_id,
get_checkpoint_metadata,
)
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.serde.types import ChannelProtocol
@@ -398,15 +399,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
checkpoint_ns = config["configurable"]["checkpoint_ns"]
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
serialized_metadata = self.jsonplus_serde.dumps(
{
**{
k: v
for k, v in config["configurable"].items()
if not k.startswith("__")
},
**config.get("metadata", {}),
**metadata,
}
get_checkpoint_metadata(config, metadata)
)
with self.cursor() as cur:
cur.execute(
@@ -16,6 +16,7 @@ from langgraph.checkpoint.base import (
CheckpointTuple,
SerializerProtocol,
get_checkpoint_id,
get_checkpoint_metadata,
)
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.serde.types import ChannelProtocol
@@ -464,15 +465,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
checkpoint_ns = config["configurable"]["checkpoint_ns"]
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
serialized_metadata = self.jsonplus_serde.dumps(
{
**{
k: v
for k, v in config["configurable"].items()
if not k.startswith("__")
},
**config.get("metadata", {}),
**metadata,
}
get_checkpoint_metadata(config, metadata)
)
async with (
self.lock,
+2 -2
View File
@@ -350,7 +350,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.10"
version = "2.0.15"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1043,4 +1043,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.1"
python-versions = "^3.9.0"
content-hash = "03c697eae6f550f3c7e29f1d61f4c409dabe04ae8d43281728e549174d2fc670"
content-hash = "e6d3ca9bce723c05f4c5ae9dc4bee872f7581b7763680b34112f1d280f5a9b0a"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-sqlite"
version = "2.0.4"
version = "2.0.5"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0"
langgraph-checkpoint = "^2.0.10"
langgraph-checkpoint = "^2.0.15"
aiosqlite = "^0.20.0"
[tool.poetry.group.dev.dependencies]
@@ -72,7 +72,6 @@ class TestAsyncSqliteSaver:
assert checkpoint.metadata == {
**self.metadata_2,
"thread_id": "thread-2",
"checkpoint_ns": "",
"run_id": "my_run_id",
}
@@ -94,14 +93,15 @@ class TestAsyncSqliteSaver:
search_results_1 = [c async for c in saver.alist(None, filter=query_1)]
assert len(search_results_1) == 1
assert search_results_1[0].metadata == {
**self.config_1["configurable"],
"thread_id": "thread-1",
"thread_ts": "1",
**self.metadata_1,
}
search_results_2 = [c async for c in saver.alist(None, filter=query_2)]
assert len(search_results_2) == 1
assert search_results_2[0].metadata == {
**self.config_2["configurable"],
"thread_id": "thread-2",
**self.metadata_2,
}
+3 -3
View File
@@ -73,7 +73,6 @@ class TestSqliteSaver:
assert checkpoint.metadata == {
**self.metadata_2,
"thread_id": "thread-2",
"checkpoint_ns": "",
"run_id": "my_run_id",
}
@@ -97,14 +96,15 @@ class TestSqliteSaver:
search_results_1 = list(saver.list(None, filter=query_1))
assert len(search_results_1) == 1
assert search_results_1[0].metadata == {
**self.config_1["configurable"],
"thread_id": "thread-1",
"thread_ts": "1",
**self.metadata_1,
}
search_results_2 = list(saver.list(None, filter=query_2))
assert len(search_results_2) == 1
assert search_results_2[0].metadata == {
**self.config_2["configurable"],
"thread_id": "thread-2",
**self.metadata_2,
}
@@ -446,6 +446,23 @@ def get_checkpoint_id(config: RunnableConfig) -> Optional[str]:
)
def get_checkpoint_metadata(
config: RunnableConfig, metadata: CheckpointMetadata
) -> CheckpointMetadata:
"""Get checkpoint metadata in a backwards-compatible manner."""
metadata = metadata.copy()
for obj in (config.get("metadata"), config.get("configurable")):
if not obj:
continue
for key in obj:
if key in metadata or key in EXCLUDED_METADATA_KEYS or key.startswith("__"):
continue
v = obj[key]
if isinstance(v, (str, int, bool, float)):
metadata[key] = v # type: ignore[literal-required]
return metadata
"""
Mapping from error type to error index.
Regular writes just map to their index in the list of writes being saved.
@@ -454,3 +471,9 @@ conflicting with regular writes.
Each Checkpointer implementation should use this mapping in put_writes.
"""
WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2, INTERRUPT: -3, RESUME: -4}
EXCLUDED_METADATA_KEYS = {
"checkpoint_id",
"checkpoint_ns",
"checkpoint_map",
}
@@ -20,6 +20,7 @@ from langgraph.checkpoint.base import (
CheckpointTuple,
SerializerProtocol,
get_checkpoint_id,
get_checkpoint_metadata,
)
from langgraph.checkpoint.serde.types import TASKS, ChannelProtocol
@@ -356,17 +357,7 @@ class InMemorySaver(
{
checkpoint["id"]: (
self.serde.dumps_typed(c),
self.serde.dumps_typed(
{
**{
k: v
for k, v in config["configurable"].items()
if not k.startswith("__")
},
**config.get("metadata", {}),
**metadata,
}
),
self.serde.dumps_typed(get_checkpoint_metadata(config, metadata)),
config["configurable"].get("checkpoint_id"), # parent
)
}
+47 -32
View File
@@ -1,7 +1,8 @@
import asyncio
import functools
import weakref
from typing import Any, Callable, Iterable, Literal, Optional, TypeVar, Union
from collections.abc import Iterable
from typing import Any, Callable, Literal, Optional, TypeVar, Union
from langgraph.store.base import (
BaseStore,
@@ -54,19 +55,23 @@ class AsyncBatchedBaseStore(BaseStore):
def __init__(self) -> None:
super().__init__()
self._loop = asyncio.get_running_loop()
self._aqueue: dict[asyncio.Future, Op] = {}
self._aqueue: asyncio.Queue[tuple[asyncio.Future, Op]] = asyncio.Queue()
self._task = self._loop.create_task(_run(self._aqueue, weakref.ref(self)))
def __del__(self) -> None:
self._task.cancel()
try:
self._task.cancel()
except RuntimeError:
pass
async def aget(
self,
namespace: tuple[str, ...],
key: str,
) -> Optional[Item]:
assert not self._task.done()
fut = self._loop.create_future()
self._aqueue[fut] = GetOp(namespace, key)
self._aqueue.put_nowait((fut, GetOp(namespace, key)))
return await fut
async def asearch(
@@ -79,8 +84,11 @@ class AsyncBatchedBaseStore(BaseStore):
limit: int = 10,
offset: int = 0,
) -> list[SearchItem]:
assert not self._task.done()
fut = self._loop.create_future()
self._aqueue[fut] = SearchOp(namespace_prefix, filter, limit, offset, query)
self._aqueue.put_nowait(
(fut, SearchOp(namespace_prefix, filter, limit, offset, query))
)
return await fut
async def aput(
@@ -90,9 +98,10 @@ class AsyncBatchedBaseStore(BaseStore):
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
) -> None:
assert not self._task.done()
_validate_namespace(namespace)
fut = self._loop.create_future()
self._aqueue[fut] = PutOp(namespace, key, value, index)
self._aqueue.put_nowait((fut, PutOp(namespace, key, value, index)))
return await fut
async def adelete(
@@ -100,8 +109,9 @@ class AsyncBatchedBaseStore(BaseStore):
namespace: tuple[str, ...],
key: str,
) -> None:
assert not self._task.done()
fut = self._loop.create_future()
self._aqueue[fut] = PutOp(namespace, key, None)
self._aqueue.put_nowait((fut, PutOp(namespace, key, None)))
return await fut
async def alist_namespaces(
@@ -113,6 +123,7 @@ class AsyncBatchedBaseStore(BaseStore):
limit: int = 100,
offset: int = 0,
) -> list[tuple[str, ...]]:
assert not self._task.done()
fut = self._loop.create_future()
match_conditions = []
if prefix:
@@ -126,7 +137,7 @@ class AsyncBatchedBaseStore(BaseStore):
limit=limit,
offset=offset,
)
self._aqueue[fut] = op
self._aqueue.put_nowait((fut, op))
return await fut
@_check_loop
@@ -250,34 +261,38 @@ def _dedupe_ops(values: list[Op]) -> tuple[Optional[list[int]], list[Op]]:
async def _run(
aqueue: dict[asyncio.Future, Op],
aqueue: asyncio.Queue[tuple[asyncio.Future, Op]],
store: weakref.ReferenceType[BaseStore],
) -> None:
while True:
await asyncio.sleep(0)
if not aqueue:
continue
while item := await aqueue.get():
# check if store is still alive
if s := store():
# get the operations to run
taken = aqueue.copy()
# action each operation
try:
values = list(taken.values())
listen, dedupped = _dedupe_ops(values)
results = await s.abatch(dedupped)
if listen is not None:
results = [results[ix] for ix in listen]
# accumulate operations scheduled in same tick
items = [item]
try:
while item := aqueue.get_nowait():
items.append(item)
except asyncio.QueueEmpty:
pass
# get the operations to run
futs = [item[0] for item in items]
values = [item[1] for item in items]
# action each operation
try:
listen, dedupped = _dedupe_ops(values)
results = await s.abatch(dedupped)
if listen is not None:
results = [results[ix] for ix in listen]
# set the results of each operation
for fut, result in zip(taken, results):
fut.set_result(result)
except Exception as e:
for fut in taken:
fut.set_exception(e)
# remove the operations from the queue
for fut in taken:
del aqueue[fut]
# set the results of each operation
for fut, result in zip(futs, results):
fut.set_result(result)
except Exception as e:
for fut in futs:
fut.set_exception(e)
finally:
# remove strong ref to store
del s
else:
break
# remove strong ref to store
del s
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
version = "2.0.13"
version = "2.0.15"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
+8 -6
View File
@@ -60,7 +60,7 @@ class TestMemorySaver:
self.metadata_3: CheckpointMetadata = {}
def test_combined_metadata(self) -> None:
config = {
config: RunnableConfig = {
"configurable": {
"thread_id": "thread-2",
"checkpoint_ns": "",
@@ -70,10 +70,10 @@ class TestMemorySaver:
}
self.memory_saver.put(config, self.chkpnt_2, self.metadata_2, {})
checkpoint = self.memory_saver.get_tuple(config)
assert checkpoint is not None
assert checkpoint.metadata == {
**self.metadata_2,
"thread_id": "thread-2",
"checkpoint_ns": "",
"run_id": "my_run_id",
}
@@ -96,14 +96,15 @@ class TestMemorySaver:
search_results_1 = list(self.memory_saver.list(None, filter=query_1))
assert len(search_results_1) == 1
assert search_results_1[0].metadata == {
**self.config_1["configurable"],
"thread_id": "thread-1",
"thread_ts": "1",
**self.metadata_1,
}
search_results_2 = list(self.memory_saver.list(None, filter=query_2))
assert len(search_results_2) == 1
assert search_results_2[0].metadata == {
**self.config_2["configurable"],
"thread_id": "thread-2",
**self.metadata_2,
}
@@ -146,7 +147,8 @@ class TestMemorySaver:
]
assert len(search_results_1) == 1
assert search_results_1[0].metadata == {
**self.config_1["configurable"],
"thread_id": "thread-1",
"thread_ts": "1",
**self.metadata_1,
}
@@ -155,7 +157,7 @@ class TestMemorySaver:
]
assert len(search_results_2) == 1
assert search_results_2[0].metadata == {
**self.config_2["configurable"],
"thread_id": "thread-2",
**self.metadata_2,
}
+16 -3
View File
@@ -33,7 +33,7 @@ from langgraph.channels.dynamic_barrier_value import DynamicBarrierValue, WaitFo
from langgraph.channels.ephemeral_value import EphemeralValue
from langgraph.channels.last_value import LastValue
from langgraph.channels.named_barrier_value import NamedBarrierValue
from langgraph.constants import EMPTY_SEQ, NS_END, NS_SEP, SELF, TAG_HIDDEN
from langgraph.constants import EMPTY_SEQ, MISSING, NS_END, NS_SEP, SELF, TAG_HIDDEN
from langgraph.errors import (
ErrorCode,
InvalidUpdateError,
@@ -691,10 +691,23 @@ class CompiledStateGraph(CompiledGraph):
updates.extend(_get_updates(i) or ())
return updates
elif get_type_hints(type(input)):
# if input is a Pydantic model, only update values
# for the keys that have been explicitly set by the users
# (this is needed to avoid sending updates for fields with None defaults)
output_keys_ = output_keys
# Pydantic v2
if hasattr(input, "model_fields_set"):
output_keys_ = [
k for k in output_keys if k in input.model_fields_set
]
# Pydantic v1
elif hasattr(input, "__fields_set__"):
output_keys_ = [k for k in output_keys if k in input.__fields_set__]
return [
(k, getattr(input, k))
for k in output_keys
if getattr(input, k, None) is not None
for k in output_keys_
if getattr(input, k, MISSING) is not MISSING
]
else:
msg = create_error_message(
@@ -197,6 +197,79 @@ class Channel:
class Pregel(PregelProtocol):
"""Pregel manages the runtime behavior for LangGraph applications.
## Channels
Channels are used to communicate between chains. Each channel has a value type,
an update type, and an update function which takes a sequence of updates and
modifies the stored value. Channels can be used to send data from one chain to
another, or to send data from a chain to itself in a future step. LangGraph
provides a number of built-in channels:
### Basic channels: LastValue and Topic
- `LastValue`: The default channel, stores the last value sent to the channel,
useful for input and output values, or for sending data from one step to the next
- `Topic`: A configurable PubSub Topic, useful for sending multiple values
between chains, or for accumulating output. Can be configured to deduplicate
values, and/or to accumulate values over the course of multiple steps.
### Advanced channels: Context and BinaryOperatorAggregate
- `Context`: exposes the value of a context manager, managing its lifecycle.
Useful for accessing external resources that require setup and/or teardown. eg.
`client = Context(httpx.Client)`
- `BinaryOperatorAggregate`: stores a persistent value, updated by applying
a binary operator to the current value and each update
sent to the channel, useful for computing aggregates over multiple steps. eg.
`total = BinaryOperatorAggregate(int, operator.add)`
## Chains
Chains are LCEL Runnables which subscribe to one or more channels, and write to
one or more channels. Any valid LCEL expression can be used as a chain. Chains
can be combined into a Pregel application, which coordinates the execution of the
chains across multiple steps.
## Pregel
Pregel combines multiple chains (or actors) into a single application. It
coordinates the execution of the chains across multiple steps, following the
Pregel/Bulk Synchronous Parallel model. Each step consists of three phases:
- **Plan**: Determine which chains to execute in this step, ie. the chains that
subscribe to channels updated in the previous step (or, in the first step,
chains that subscribe to input channels)
- **Execution**: Execute those chains in parallel, until all complete, or one fails,
or a timeout is reached. Any channel updates are invisible to other
chains until the next step.
- **Update**: Update the channels with the values written by the
chains in this step.
Repeat until no chains are planned for execution, or a maximum number of steps
is reached.
Example:
```python
from langgraph import Channel, Pregel
grow_value = (
Channel.subscribe_to("value")
| (lambda x: x + x)
| Channel.write_to(value=lambda x: x if len(x) < 10 else None)
)
app = Pregel(
chains={"grow_value": grow_value},
input="value",
output="value",
)
assert app.invoke("a") == "aaaaaaaa"
```
"""
nodes: dict[str, PregelNode]
channels: dict[str, Union[BaseChannel, ManagedValueSpec]]
+44 -4
View File
@@ -13,6 +13,7 @@ from typing import (
Coroutine,
Iterator,
Optional,
Protocol,
Sequence,
Tuple,
Union,
@@ -35,7 +36,7 @@ from langchain_core.runnables.config import (
)
from langchain_core.runnables.utils import Input, Output
from langchain_core.tracers._streaming import _StreamingCallbackHandler
from typing_extensions import Concatenate, ParamSpec, TypeGuard
from typing_extensions import TypeGuard
from langgraph.constants import (
CONF,
@@ -132,12 +133,51 @@ Each tuple contains:
VALID_KINDS = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
P = ParamSpec("P") # to handle injected kwargs like `writer` / `store`
class _RunnableWithWriter(Protocol[Input, Output]):
def __call__(self, state: Input, *, writer: StreamWriter) -> Output: ...
class _RunnableWithStore(Protocol[Input, Output]):
def __call__(self, state: Input, *, store: BaseStore) -> Output: ...
class _RunnableWithWriterStore(Protocol[Input, Output]):
def __call__(
self, state: Input, *, writer: StreamWriter, store: BaseStore
) -> Output: ...
class _RunnableWithConfigWriter(Protocol[Input, Output]):
def __call__(
self, state: Input, *, config: RunnableConfig, writer: StreamWriter
) -> Output: ...
class _RunnableWithConfigStore(Protocol[Input, Output]):
def __call__(
self, state: Input, *, config: RunnableConfig, store: BaseStore
) -> Output: ...
class _RunnableWithConfigWriterStore(Protocol[Input, Output]):
def __call__(
self,
state: Input,
*,
config: RunnableConfig,
writer: StreamWriter,
store: BaseStore,
) -> Output: ...
RunnableLike = Union[
LCRunnableLike,
Callable[Concatenate[Input, P], Output],
Callable[Concatenate[Input, P], Awaitable[Output]],
_RunnableWithWriter[Input, Output],
_RunnableWithStore[Input, Output],
_RunnableWithWriterStore[Input, Output],
_RunnableWithConfigWriter[Input, Output],
_RunnableWithConfigStore[Input, Output],
_RunnableWithConfigWriterStore[Input, Output],
]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.2.71"
version = "0.2.73"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
File diff suppressed because it is too large Load Diff
@@ -133,8 +133,6 @@ async def test_invoke_two_processes_in_out_interrupt(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 6,
@@ -158,8 +156,6 @@ async def test_invoke_two_processes_in_out_interrupt(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 5,
@@ -183,8 +179,6 @@ async def test_invoke_two_processes_in_out_interrupt(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "input",
"step": 4,
@@ -206,8 +200,6 @@ async def test_invoke_two_processes_in_out_interrupt(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 3,
@@ -231,8 +223,6 @@ async def test_invoke_two_processes_in_out_interrupt(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "input",
"step": 2,
@@ -254,8 +244,6 @@ async def test_invoke_two_processes_in_out_interrupt(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -279,8 +267,6 @@ async def test_invoke_two_processes_in_out_interrupt(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -304,7 +290,6 @@ async def test_invoke_two_processes_in_out_interrupt(
}
},
metadata={
"checkpoint_ns": "",
"parents": {},
"source": "input",
"step": -1,
@@ -381,8 +366,6 @@ async def test_fork_always_re_runs_nodes(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 5,
@@ -404,8 +387,6 @@ async def test_fork_always_re_runs_nodes(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 4,
@@ -427,8 +408,6 @@ async def test_fork_always_re_runs_nodes(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 3,
@@ -450,8 +429,6 @@ async def test_fork_always_re_runs_nodes(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 2,
@@ -473,8 +450,6 @@ async def test_fork_always_re_runs_nodes(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -496,8 +471,6 @@ async def test_fork_always_re_runs_nodes(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -521,7 +494,6 @@ async def test_fork_always_re_runs_nodes(
}
},
metadata={
"checkpoint_ns": "",
"parents": {},
"source": "input",
"step": -1,
@@ -862,8 +834,6 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
await app_w_interrupt.checkpointer.aget_tuple(config)
).checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -924,8 +894,6 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 1,
@@ -1050,8 +1018,6 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 4,
@@ -1134,8 +1100,6 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -1196,8 +1160,6 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 1,
@@ -1322,8 +1284,6 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 4,
@@ -1406,8 +1366,6 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -1840,8 +1798,6 @@ async def test_conditional_graph_state(
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -1897,8 +1853,6 @@ async def test_conditional_graph_state(
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 2,
@@ -1989,8 +1943,6 @@ async def test_conditional_graph_state(
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 5,
@@ -2058,8 +2010,6 @@ async def test_conditional_graph_state(
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -2114,8 +2064,6 @@ async def test_conditional_graph_state(
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 2,
@@ -2204,8 +2152,6 @@ async def test_conditional_graph_state(
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 5,
@@ -2822,8 +2768,6 @@ async def test_state_graph_packets(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -2883,8 +2827,6 @@ async def test_state_graph_packets(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 2,
@@ -2994,8 +2936,6 @@ async def test_state_graph_packets(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 4,
@@ -3065,8 +3005,6 @@ async def test_state_graph_packets(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 5,
@@ -3141,8 +3079,6 @@ async def test_state_graph_packets(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -3204,8 +3140,6 @@ async def test_state_graph_packets(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 2,
@@ -3315,8 +3249,6 @@ async def test_state_graph_packets(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 4,
@@ -3386,8 +3318,6 @@ async def test_state_graph_packets(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 5,
@@ -3645,8 +3575,6 @@ async def test_message_graph(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -3701,8 +3629,6 @@ async def test_message_graph(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 2,
@@ -3793,8 +3719,6 @@ async def test_message_graph(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 4,
@@ -3855,8 +3779,6 @@ async def test_message_graph(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 5,
@@ -4165,8 +4087,6 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
if "shallow" not in checkpointer_name:
assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [
{
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -4175,7 +4095,6 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
"thread_id": "1",
},
{
"checkpoint_ns": "",
"parents": {},
"source": "input",
"step": -1,
@@ -4198,8 +4117,6 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -4233,8 +4150,6 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -4270,8 +4185,6 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -4305,8 +4218,6 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -4342,8 +4253,6 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -4374,8 +4283,6 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 1,
@@ -4409,8 +4316,6 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 2,
@@ -4951,8 +4856,6 @@ async def test_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -4985,8 +4888,6 @@ async def test_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 3,
@@ -5021,8 +4922,6 @@ async def test_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -5055,8 +4954,6 @@ async def test_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 3,
@@ -5099,8 +4996,6 @@ async def test_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -5133,8 +5028,6 @@ async def test_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 3,
@@ -5169,8 +5062,6 @@ async def test_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -5203,8 +5094,6 @@ async def test_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 3,
@@ -5233,7 +5122,6 @@ async def test_branch_then(checkpointer_name: str) -> None:
config=uconfig,
created_at=AnyStr(),
metadata={
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 0,
@@ -5261,8 +5149,6 @@ async def test_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -5289,8 +5175,6 @@ async def test_branch_then(checkpointer_name: str) -> None:
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 3,
@@ -5382,8 +5266,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"outer_1": {"my_key": "hi my value"}},
@@ -5435,8 +5317,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict({"": AnyStr()}),
"parents": {
"": AnyStr(),
},
@@ -5483,8 +5363,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"outer_1": {"my_key": "hi my value"}},
@@ -5531,8 +5409,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"outer_1": {"my_key": "hi my value"}},
@@ -5571,8 +5447,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": None,
@@ -5607,7 +5481,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_ns": "",
"parents": {},
"source": "input",
"writes": {"__start__": {"my_key": "my value"}},
@@ -5643,8 +5516,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict({"": AnyStr()}),
"source": "loop",
"writes": {
"inner_1": {
@@ -5693,8 +5564,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict({"": AnyStr()}),
"source": "loop",
"writes": None,
"step": 0,
@@ -5744,8 +5613,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": None,
"checkpoint_map": AnyDict({"": AnyStr()}),
"source": "input",
"writes": {"__start__": {"my_key": "hi my value"}},
"step": -1,
@@ -5791,8 +5658,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {
@@ -5829,8 +5694,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {
@@ -5873,8 +5736,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"inner": {"my_key": "hi my value here and there"}},
@@ -5915,8 +5776,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"outer_1": {"my_key": "hi my value"}},
@@ -5951,8 +5810,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": None,
@@ -5987,7 +5844,6 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_ns": "",
"parents": {},
"source": "input",
"writes": {"__start__": {"my_key": "my value"}},
@@ -6097,8 +5953,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"parent_1": {"my_key": "hi my value"}},
@@ -6145,8 +5999,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {"": AnyStr()},
"source": "loop",
"writes": None,
@@ -6193,8 +6045,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict({"": AnyStr(), AnyStr("child:"): AnyStr()}),
"parents": AnyDict(
{
"": AnyStr(),
@@ -6274,10 +6124,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{"": AnyStr(), AnyStr("child:"): AnyStr()}
),
"parents": AnyDict(
{
"": AnyStr(),
@@ -6340,8 +6186,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict({"": AnyStr()}),
"parents": {"": AnyStr()},
"source": "loop",
"writes": None,
@@ -6381,8 +6225,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"parent_1": {"my_key": "hi my value"}},
@@ -6431,8 +6273,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {
@@ -6478,8 +6318,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {
@@ -6510,8 +6348,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"child": {"my_key": "hi my value here and there"}},
@@ -6556,8 +6392,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"parent_1": {"my_key": "hi my value"}},
@@ -6615,7 +6449,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_ns": "",
"parents": {},
"source": "input",
"writes": {"my_key": "my value"},
@@ -6651,8 +6484,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict({"": AnyStr()}),
"source": "loop",
"writes": {"child_1": {"my_key": "hi my value here and there"}},
"step": 1,
@@ -6692,8 +6523,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict({"": AnyStr()}),
"source": "loop",
"writes": None,
"step": 0,
@@ -6746,8 +6575,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": None,
"checkpoint_map": AnyDict({"": AnyStr()}),
"source": "input",
"writes": {"__start__": {"my_key": "hi my value"}},
"step": -1,
@@ -6795,10 +6622,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{"": AnyStr(), AnyStr("child:"): AnyStr()}
),
"source": "loop",
"writes": {
"grandchild_2": {"my_key": "hi my value here and there"}
@@ -6856,10 +6679,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{"": AnyStr(), AnyStr("child:"): AnyStr()}
),
"source": "loop",
"writes": {"grandchild_1": {"my_key": "hi my value here"}},
"step": 1,
@@ -6922,10 +6741,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{"": AnyStr(), AnyStr("child:"): AnyStr()}
),
"source": "loop",
"writes": None,
"step": 0,
@@ -6988,10 +6803,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": None,
"checkpoint_map": AnyDict(
{"": AnyStr(), AnyStr("child:"): AnyStr()}
),
"source": "input",
"writes": {"__start__": {"my_key": "hi my value"}},
"step": -1,
@@ -7270,8 +7081,6 @@ async def test_weather_subgraph(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"source": "loop",
"writes": {"router_node": {"route": "weather"}},
"step": 1,
@@ -7368,8 +7177,6 @@ async def test_weather_subgraph(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"source": "loop",
"writes": {"router_node": {"route": "weather"}},
"step": 1,
@@ -7415,8 +7222,6 @@ async def test_weather_subgraph(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict({"": AnyStr()}),
"source": "loop",
"writes": {"model_node": {"city": "San Francisco"}},
"step": 1,
@@ -7480,8 +7285,6 @@ async def test_weather_subgraph(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"source": "loop",
"writes": {"router_node": {"route": "weather"}},
"step": 1,
@@ -7528,7 +7331,6 @@ async def test_weather_subgraph(
}
},
metadata={
"checkpoint_map": AnyDict({"": AnyStr()}),
"step": 2,
"source": "update",
"writes": {
-5
View File
@@ -63,7 +63,6 @@ from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore
from langgraph.types import Command, Interrupt, interrupt
from langgraph.utils.config import get_stream_writer
from tests.any_str import AnyStr
from tests.conftest import (
ALL_CHECKPOINTERS_ASYNC,
ALL_CHECKPOINTERS_SYNC,
@@ -183,8 +182,6 @@ def test_no_prompt(
"agent": "agent",
}
assert saved.metadata == {
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
@@ -217,8 +214,6 @@ async def test_no_prompt_async(checkpointer_name: str) -> None:
"agent": "agent",
}
assert saved.metadata == {
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
+13 -11
View File
@@ -1112,8 +1112,6 @@ def test_pending_writes_resume(
PregelTask(AnyStr(), "two", (PULL, "two"), 'ConnectionError("I\'m not good")'),
)
assert state.metadata == {
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -1212,8 +1210,6 @@ def test_pending_writes_resume(
"channel_values": {"one": "one", "two": "two", "value": 6},
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"step": 1,
"source": "loop",
@@ -1264,8 +1260,6 @@ def test_pending_writes_resume(
},
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"step": 0,
"source": "loop",
@@ -1307,7 +1301,6 @@ def test_pending_writes_resume(
"channel_values": {"__start__": {"value": 1}},
},
metadata={
"checkpoint_ns": "",
"parents": {},
"step": -1,
"source": "input",
@@ -2398,8 +2391,6 @@ def test_in_one_fan_out_state_graph_waiting_edge(
},
created_at=AnyStr(),
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 4,
@@ -4835,8 +4826,6 @@ def test_parent_command(request: pytest.FixtureRequest, checkpointer_name: str)
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"source": "loop",
"writes": {
"alice": {
@@ -6493,3 +6482,16 @@ def test_node_destinations() -> None:
Edge(source="child", target="node_b", data="foo", conditional=True),
Edge(source="child", target="node_c", data="bar", conditional=True),
] == graph.edges
def test_pydantic_none_state_update() -> None:
from pydantic import BaseModel
class State(BaseModel):
foo: Optional[str]
def node_a(state: State) -> State:
return State(foo=None)
graph = StateGraph(State).add_node(node_a).add_edge(START, "node_a").compile()
assert graph.invoke({"foo": ""}) == {"foo": None}
-59
View File
@@ -606,8 +606,6 @@ async def test_dynamic_interrupt(checkpointer_name: str) -> None:
if "shallow" not in checkpointer_name:
assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [
{
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -615,7 +613,6 @@ async def test_dynamic_interrupt(checkpointer_name: str) -> None:
"thread_id": "1",
},
{
"checkpoint_ns": "",
"parents": {},
"source": "input",
"step": -1,
@@ -644,8 +641,6 @@ async def test_dynamic_interrupt(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -672,8 +667,6 @@ async def test_dynamic_interrupt(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 1,
@@ -798,8 +791,6 @@ async def test_dynamic_interrupt_subgraph(checkpointer_name: str) -> None:
c.metadata async for c in tool_two.checkpointer.alist(thread1root)
] == [
{
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -807,7 +798,6 @@ async def test_dynamic_interrupt_subgraph(checkpointer_name: str) -> None:
"thread_id": "1",
},
{
"checkpoint_ns": "",
"parents": {},
"source": "input",
"step": -1,
@@ -842,8 +832,6 @@ async def test_dynamic_interrupt_subgraph(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -870,8 +858,6 @@ async def test_dynamic_interrupt_subgraph(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "update",
"step": 1,
@@ -990,8 +976,6 @@ async def test_copy_checkpoint(checkpointer_name: str) -> None:
if "shallow" not in checkpointer_name:
assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [
{
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -999,7 +983,6 @@ async def test_copy_checkpoint(checkpointer_name: str) -> None:
"thread_id": "1",
},
{
"checkpoint_ns": "",
"parents": {},
"source": "input",
"step": -1,
@@ -1038,8 +1021,6 @@ async def test_copy_checkpoint(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -1083,8 +1064,6 @@ async def test_copy_checkpoint(checkpointer_name: str) -> None:
config=tup.config,
created_at=tup.checkpoint["ts"],
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "fork",
"step": 1,
@@ -1251,8 +1230,6 @@ async def test_cancel_graph_astream(checkpointer_name: str) -> None:
assert state.values == {"value": 3} # 1 + 2
assert state.next == ("aparallelwhile",)
assert state.metadata == {
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -1330,8 +1307,6 @@ async def test_cancel_graph_astream_events_v2(checkpointer_name: Optional[str])
assert state.values == {"value": 2}
assert state.next == ("awhile",)
assert state.metadata == {
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 1,
@@ -2032,8 +2007,6 @@ async def test_pending_writes_resume(
),
)
assert state.metadata == {
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"step": 0,
@@ -2132,8 +2105,6 @@ async def test_pending_writes_resume(
"channel_values": {"one": "one", "two": "two", "value": 6},
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"step": 1,
"source": "loop",
@@ -2186,8 +2157,6 @@ async def test_pending_writes_resume(
},
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"step": 0,
"source": "loop",
@@ -2231,7 +2200,6 @@ async def test_pending_writes_resume(
"channel_values": {"__start__": {"value": 1}},
},
metadata={
"checkpoint_ns": "",
"parents": {},
"step": -1,
"source": "input",
@@ -2810,8 +2778,6 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"source": "loop",
"writes": {"3": ["3"]},
"thread_id": "1",
@@ -2848,8 +2814,6 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"source": "loop",
"writes": {"2": ["2|3"], "3": ["3"], "flaky": ["flaky|4"]},
"thread_id": "1",
@@ -2893,8 +2857,6 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"source": "loop",
"writes": {
"2": [
@@ -2960,8 +2922,6 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"source": "loop",
"writes": {"1": ["1"]},
"thread_id": "1",
@@ -3017,8 +2977,6 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"source": "loop",
"writes": None,
"thread_id": "1",
@@ -3056,7 +3014,6 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_ns": "",
"source": "input",
"writes": {"__start__": ["0"]},
"thread_id": "1",
@@ -3229,8 +3186,6 @@ async def test_send_react_interrupt(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"step": 1,
"source": "loop",
"writes": {
@@ -3302,8 +3257,6 @@ async def test_send_react_interrupt(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"step": 2,
"source": "update",
"writes": {
@@ -3391,8 +3344,6 @@ async def test_send_react_interrupt(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"step": 1,
"source": "loop",
"writes": {
@@ -3485,8 +3436,6 @@ async def test_send_react_interrupt(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"step": 2,
"source": "update",
"writes": {
@@ -3702,8 +3651,6 @@ async def test_send_react_interrupt_control(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"step": 1,
"source": "loop",
"writes": {
@@ -3775,8 +3722,6 @@ async def test_send_react_interrupt_control(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"step": 2,
"source": "update",
"writes": {
@@ -4726,8 +4671,6 @@ async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class(
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"parents": {},
"source": "loop",
"writes": {"qa": {"answer": "doc1,doc2,doc3,doc4"}},
@@ -6202,8 +6145,6 @@ async def test_parent_command(checkpointer_name: str) -> None:
}
},
metadata={
"checkpoint_id": AnyStr(),
"checkpoint_ns": "",
"source": "loop",
"writes": {
"alice": {
+1
View File
@@ -13,3 +13,4 @@ react.d.cts
node_modules
dist
.yarn
docs
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.40",
"version": "0.0.42",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
@@ -10,7 +10,8 @@
"prepublish": "yarn run build",
"format": "prettier --write src",
"lint": "prettier --check src && tsc --noEmit",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts"
"test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts",
"typedoc": "typedoc && typedoc src/react/index.ts --out docs/react --options typedoc.react.json"
},
"main": "index.js",
"license": "MIT",
@@ -33,8 +34,8 @@
"jest": "^29.7.0",
"prettier": "^3.2.5",
"ts-jest": "^29.1.2",
"typedoc": "^0.26.1",
"typedoc-plugin-markdown": "^4.1.0",
"typedoc": "^0.27.7",
"typedoc-plugin-markdown": "^4.4.2",
"typescript": "^5.4.5",
"react": "^18.3.1"
},
+347 -197
View File
@@ -128,13 +128,145 @@ interface ValidSequence<StateType = any> {
}
export type MessageMetadata<StateType extends Record<string, unknown>> = {
/**
* The ID of the message used.
*/
messageId: string;
/**
* The first thread state the message was seen in.
*/
firstSeenState: ThreadState<StateType> | undefined;
/**
* The branch of the message.
*/
branch: string | undefined;
/**
* The list of branches this message is part of.
* This is useful for displaying branching controls.
*/
branchOptions: string[] | undefined;
};
function getBranchSequence<StateType extends Record<string, unknown>>(
history: ThreadState<StateType>[],
) {
const childrenMap: Record<string, ThreadState<StateType>[]> = {};
// First pass - collect nodes for each checkpoint
history.forEach((state) => {
const checkpointId = state.parent_checkpoint?.checkpoint_id ?? "$";
childrenMap[checkpointId] ??= [];
childrenMap[checkpointId].push(state);
});
// Second pass - create a tree of sequences
type Task = { id: string; sequence: Sequence; path: string[] };
const rootSequence: Sequence = { type: "sequence", items: [] };
const queue: Task[] = [{ id: "$", sequence: rootSequence, path: [] }];
const paths: string[][] = [];
const visited = new Set<string>();
while (queue.length > 0) {
const task = queue.shift()!;
if (visited.has(task.id)) continue;
visited.add(task.id);
const children = childrenMap[task.id];
if (children == null || children.length === 0) continue;
// If we've encountered a fork (2+ children), push the fork
// to the sequence and add a new sequence for each child
let fork: Fork | undefined;
if (children.length > 1) {
fork = { type: "fork", items: [] };
task.sequence.items.push(fork);
}
for (const value of children) {
const id = value.checkpoint.checkpoint_id!;
let sequence = task.sequence;
let path = task.path;
if (fork != null) {
sequence = { type: "sequence", items: [] };
fork.items.unshift(sequence);
path = path.slice();
path.push(id);
paths.push(path);
}
sequence.items.push({ type: "node", value, path });
queue.push({ id, sequence, path });
}
}
return { rootSequence, paths };
}
const PATH_SEP = ">";
const ROOT_ID = "$";
// Get flat view
function getBranchView<StateType extends Record<string, unknown>>(
sequence: Sequence<StateType>,
paths: string[][],
branch: string,
) {
const path = branch.split(PATH_SEP);
const pathMap: Record<string, string[][]> = {};
for (const path of paths) {
const parent = path.at(-2) ?? ROOT_ID;
pathMap[parent] ??= [];
pathMap[parent].unshift(path);
}
const history: ThreadState<StateType>[] = [];
const branchByCheckpoint: Record<
string,
{ branch: string | undefined; branchOptions: string[] | undefined }
> = {};
const forkStack = path.slice();
const queue: (Node<StateType> | Fork<StateType>)[] = [...sequence.items];
while (queue.length > 0) {
const item = queue.shift()!;
if (item.type === "node") {
history.push(item.value);
branchByCheckpoint[item.value.checkpoint.checkpoint_id!] = {
branch: item.path.join(PATH_SEP),
branchOptions: (item.path.length > 0
? pathMap[item.path.at(-2) ?? ROOT_ID] ?? []
: []
).map((p) => p.join(PATH_SEP)),
};
}
if (item.type === "fork") {
const forkId = forkStack.shift();
const index =
forkId != null
? item.items.findIndex((value) => {
const firstItem = value.items.at(0);
if (!firstItem || firstItem.type !== "node") return false;
return firstItem.value.checkpoint.checkpoint_id === forkId;
})
: -1;
const nextItems = item.items.at(index)?.items ?? [];
queue.push(...nextItems);
}
}
return { history, branchByCheckpoint };
}
function fetchHistory<StateType extends Record<string, unknown>>(
client: Client,
threadId: string,
@@ -179,29 +311,189 @@ function useThreadHistory<StateType extends Record<string, unknown>>(
};
}
const useControllableThreadId = (options?: {
threadId?: string | null;
onThreadId?: (threadId: string) => void;
}): [string | null, (threadId: string) => void] => {
const [localThreadId, _setLocalThreadId] = useState<string | null>(
options?.threadId ?? null,
);
const onThreadIdRef = useRef(options?.onThreadId);
onThreadIdRef.current = options?.onThreadId;
const onThreadId = useCallback((threadId: string) => {
_setLocalThreadId(threadId);
onThreadIdRef.current?.(threadId);
}, []);
if (typeof options?.threadId === "undefined") {
return [localThreadId, onThreadId];
}
return [options.threadId, onThreadId];
};
interface UseStreamOptions<
StateType extends Record<string, unknown> = Record<string, unknown>,
UpdateType extends Record<string, unknown> = Partial<StateType>,
CustomType = unknown,
> {
/**
* The ID of the assistant to use.
*/
assistantId: string;
/**
* The URL of the API to use.
*/
apiUrl: ClientConfig["apiUrl"];
/**
* The API key to use.
*/
apiKey?: ClientConfig["apiKey"];
/**
* Specify the key within the state that contains messages.
* Defaults to "messages".
*
* @default "messages"
*/
messagesKey?: string;
/**
* Callback that is called when an error occurs.
*/
onError?: (error: unknown) => void;
/**
* Callback that is called when the stream is finished.
*/
onFinish?: (state: ThreadState<StateType>) => void;
/**
* Callback that is called when an update event is received.
*/
onUpdateEvent?: (data: UpdatesStreamEvent<UpdateType>["data"]) => void;
/**
* Callback that is called when a custom event is received.
*/
onCustomEvent?: (data: CustomStreamEvent<CustomType>["data"]) => void;
/**
* Callback that is called when a metadata event is received.
*/
onMetadataEvent?: (data: MetadataStreamEvent["data"]) => void;
/**
* The ID of the thread to fetch history and current values from.
*/
threadId?: string | null;
/**
* Callback that is called when the thread ID is updated (ie when a new thread is created).
*/
onThreadId?: (threadId: string) => void;
}
interface UseStream<
StateType extends Record<string, unknown> = Record<string, unknown>,
UpdateType extends Record<string, unknown> = Partial<StateType>,
> {
/**
* The current values of the thread.
*/
values: StateType;
/**
* Last seen error from the thread or during streaming.
*/
error: unknown;
/**
* Whether the stream is currently running.
*/
isLoading: boolean;
/**
* Stops the stream.
*/
stop: () => void;
/**
* Create and stream a run to the thread.
*/
submit: (values: UpdateType, options?: SubmitOptions<StateType>) => void;
/**
* The current branch of the thread.
*/
branch: string;
/**
* Set the branch of the thread.
*/
setBranch: (branch: string) => void;
/**
* Flattened history of thread states of a thread.
*/
history: ThreadState<StateType>[];
/**
* Tree of all branches for the thread.
* @experimental
*/
experimental_branchTree: Sequence<StateType>;
/**
* Messages inferred from the thread.
* Will automatically update with incoming message chunks.
*/
messages: Message[];
/**
* Get the metadata for a message, such as first thread state the message
* was seen in and branch information.
* @param message - The message to get the metadata for.
* @param index - The index of the message in the thread.
* @returns The metadata for the message.
*/
getMessagesMetadata: (
message: Message,
index?: number,
) => MessageMetadata<StateType> | undefined;
}
interface SubmitOptions<
StateType extends Record<string, unknown> = Record<string, unknown>,
> {
config?: Config;
checkpoint?: Omit<Checkpoint, "thread_id"> | null;
command?: Command;
interruptBefore?: "*" | string[];
interruptAfter?: "*" | string[];
metadata?: Metadata;
multitaskStrategy?: MultitaskStrategy;
onCompletion?: OnCompletionBehavior;
onDisconnect?: DisconnectMode;
feedbackKeys?: string[];
streamMode?: Array<StreamMode>;
optimisticValues?:
| Partial<StateType>
| ((prev: StateType) => Partial<StateType>);
}
export function useStream<
StateType extends Record<string, unknown> = Record<string, unknown>,
UpdateType extends Record<string, unknown> = Partial<StateType>,
CustomType = unknown,
>(options: {
assistantId: string;
apiUrl: ClientConfig["apiUrl"];
apiKey?: ClientConfig["apiKey"];
withMessages?: string;
onError?: (error: unknown) => void;
onFinish?: (state: ThreadState<StateType>) => void;
onUpdateEvent?: (data: UpdatesStreamEvent<UpdateType>["data"]) => void;
onCustomEvent?: (data: CustomStreamEvent<CustomType>["data"]) => void;
onMetadataEvent?: (data: MetadataStreamEvent["data"]) => void;
// TODO: can we make threadId uncontrollable / controllable?
threadId?: string | null;
onThreadId?: (threadId: string) => void;
}) {
>(
options: UseStreamOptions<StateType, UpdateType, CustomType>,
): UseStream<StateType, UpdateType> {
type EventStreamEvent =
| ValuesStreamEvent<StateType>
| UpdatesStreamEvent<UpdateType>
@@ -214,15 +506,17 @@ export function useStream<
| ErrorStreamEvent
| FeedbackStreamEvent;
const { assistantId, threadId, withMessages, onError, onFinish } = options;
let { assistantId, messagesKey, onError, onFinish } = options;
messagesKey ??= "messages";
const client = useMemo(
() => new Client({ apiUrl: options.apiUrl, apiKey: options.apiKey }),
[options.apiKey, options.apiUrl],
);
const [threadId, onThreadId] = useControllableThreadId(options);
const [branchPath, setBranchPath] = useState<string[]>([]);
const [branch, setBranch] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const [_, setEvents] = useState<EventStreamEvent[]>([]);
const [streamError, setStreamError] = useState<unknown>(undefined);
const [streamValues, setStreamValues] = useState<StateType | null>(null);
@@ -269,118 +563,20 @@ export function useStream<
);
const getMessages = useMemo(() => {
if (withMessages == null) return undefined;
return (value: StateType) =>
Array.isArray(value[withMessages])
? (value[withMessages] as Message[])
Array.isArray(value[messagesKey])
? (value[messagesKey] as Message[])
: [];
}, [withMessages]);
}, [messagesKey]);
const [sequence, pathMap] = (() => {
const childrenMap: Record<string, ThreadState<StateType>[]> = {};
const { rootSequence, paths } = getBranchSequence(history.data);
const { history: flatHistory, branchByCheckpoint } = getBranchView(
rootSequence,
paths,
branch,
);
// First pass - collect nodes for each checkpoint
history.data.forEach((state) => {
const checkpointId = state.parent_checkpoint?.checkpoint_id ?? "$";
childrenMap[checkpointId] ??= [];
childrenMap[checkpointId].push(state);
});
// Second pass - create a tree of sequences
type Task = { id: string; sequence: Sequence; path: string[] };
const rootSequence: Sequence = { type: "sequence", items: [] };
const queue: Task[] = [{ id: "$", sequence: rootSequence, path: [] }];
const paths: string[][] = [];
const visited = new Set<string>();
while (queue.length > 0) {
const task = queue.shift()!;
if (visited.has(task.id)) continue;
visited.add(task.id);
const children = childrenMap[task.id];
if (children == null || children.length === 0) continue;
// If we've encountered a fork (2+ children), push the fork
// to the sequence and add a new sequence for each child
let fork: Fork | undefined;
if (children.length > 1) {
fork = { type: "fork", items: [] };
task.sequence.items.push(fork);
}
for (const value of children) {
const id = value.checkpoint.checkpoint_id!;
let sequence = task.sequence;
let path = task.path;
if (fork != null) {
sequence = { type: "sequence", items: [] };
fork.items.unshift(sequence);
path = path.slice();
path.push(id);
paths.push(path);
}
sequence.items.push({ type: "node", value, path });
queue.push({ id, sequence, path });
}
}
// Third pass, create a map for available forks
const pathMap: Record<string, string[][]> = {};
for (const path of paths) {
const parent = path.at(-2) ?? "$";
pathMap[parent] ??= [];
pathMap[parent].unshift(path);
}
return [rootSequence as ValidSequence, pathMap];
})();
const [flatValues, flatPaths] = (() => {
const result: ThreadState<StateType>[] = [];
const flatPaths: Record<
string,
{ current: string[] | undefined; branches: string[][] | undefined }
> = {};
const forkStack = branchPath.slice();
const queue: (Node<StateType> | Fork<StateType>)[] = [...sequence.items];
while (queue.length > 0) {
const item = queue.shift()!;
if (item.type === "node") {
result.push(item.value);
flatPaths[item.value.checkpoint.checkpoint_id!] = {
current: item.path,
branches:
item.path.length > 0 ? pathMap[item.path.at(-2) ?? "$"] ?? [] : [],
};
}
if (item.type === "fork") {
const forkId = forkStack.shift();
const index =
forkId != null
? item.items.findIndex((value) => {
const firstItem = value.items.at(0);
if (!firstItem || firstItem.type !== "node") return false;
return firstItem.value.checkpoint.checkpoint_id === forkId;
})
: -1;
const nextItems = item.items.at(index)?.items ?? [];
queue.push(...nextItems);
}
}
return [result, flatPaths];
})();
const threadHead: ThreadState<StateType> | undefined = flatValues.at(-1);
const threadHead: ThreadState<StateType> | undefined = flatHistory.at(-1);
const historyValues = threadHead?.values ?? ({} as StateType);
const historyError = (() => {
const error = threadHead?.tasks?.at(-1)?.error;
@@ -399,8 +595,6 @@ export function useStream<
})();
const messageMetadata = (() => {
if (getMessages == null) return undefined;
const alreadyShown = new Set<string>();
return getMessages(historyValues).map(
(message, idx): MessageMetadata<StateType> => {
@@ -416,13 +610,13 @@ export function useStream<
| undefined;
let branch = firstSeen
? flatPaths[firstSeen.checkpoint.checkpoint_id!]
? branchByCheckpoint[firstSeen.checkpoint.checkpoint_id!]
: undefined;
if (!branch?.current?.length) branch = undefined;
if (!branch?.branch?.length) branch = undefined;
// serialize branches
const optionsShown = branch?.branches?.flat(2).join(",");
const optionsShown = branch?.branchOptions?.flat(2).join(",");
if (optionsShown) {
if (alreadyShown.has(optionsShown)) branch = undefined;
alreadyShown.add(optionsShown);
@@ -431,8 +625,9 @@ export function useStream<
return {
messageId: messageId.toString(),
firstSeenState: firstSeen,
branch: branch?.current?.join(">"),
branchOptions: branch?.branches?.map((b) => b.join(">")),
branch: branch?.branch,
branchOptions: branch?.branchOptions,
};
},
);
@@ -445,22 +640,7 @@ export function useStream<
const submit = async (
values: UpdateType | undefined,
submitOptions?: {
config?: Config;
checkpoint?: Omit<Checkpoint, "thread_id"> | null;
command?: Command;
interruptBefore?: "*" | string[];
interruptAfter?: "*" | string[];
metadata?: Metadata;
multitaskStrategy?: MultitaskStrategy;
onCompletion?: OnCompletionBehavior;
onDisconnect?: DisconnectMode;
feedbackKeys?: string[];
streamMode?: Array<StreamMode>;
optimisticValues?:
| Partial<StateType>
| ((prev: StateType) => Partial<StateType>);
},
submitOptions?: SubmitOptions<StateType>,
) => {
try {
setIsLoading(true);
@@ -472,7 +652,7 @@ export function useStream<
let usableThreadId = threadId;
if (!usableThreadId) {
const thread = await client.threads.create();
options?.onThreadId?.(thread.thread_id);
onThreadId(thread.thread_id);
usableThreadId = thread.thread_id;
}
@@ -507,9 +687,10 @@ export function useStream<
// Unbranch things
const newPath = submitOptions?.checkpoint?.checkpoint_id
? flatPaths[submitOptions?.checkpoint?.checkpoint_id]?.current
? branchByCheckpoint[submitOptions?.checkpoint?.checkpoint_id]?.branch
: undefined;
if (newPath != null) setBranchPath(newPath ?? []);
if (newPath != null) setBranch(newPath ?? "");
// Assumption: we're setting the initial value
// Used for instant feedback
@@ -530,32 +711,17 @@ export function useStream<
let streamError: StreamError | undefined;
for await (const { event, data } of run) {
setEvents((events) => [...events, { event, data } as EventStreamEvent]);
if (event === "error") {
streamError = new StreamError(data);
break;
}
if (event === "updates") {
options.onUpdateEvent?.(data);
}
if (event === "custom") {
options.onCustomEvent?.(data);
}
if (event === "metadata") {
options.onMetadataEvent?.(data);
}
if (event === "values") {
setStreamValues(data);
}
if (event === "updates") options.onUpdateEvent?.(data);
if (event === "custom") options.onCustomEvent?.(data);
if (event === "metadata") options.onMetadataEvent?.(data);
if (event === "values") setStreamValues(data);
if (event === "messages") {
if (!getMessages) continue;
const [serialized] = data;
const messageId = messageManagerRef.current.add(serialized);
@@ -577,15 +743,13 @@ export function useStream<
if (!chunk || index == null) return values;
messages[index] = toMessageDict(chunk);
return { ...values, [withMessages!]: messages };
return { ...values, [messagesKey!]: messages };
});
}
}
// TODO: stream created checkpoints to avoid an unnecessary network request
const result = await history.mutate(usableThreadId);
// TODO: write tests verifying that stream values are properly handled lifecycle-wise
setStreamValues(null);
if (streamError != null) throw streamError;
@@ -615,11 +779,6 @@ export function useStream<
const error = isLoading ? streamError : historyError;
const values = streamValues ?? historyValues;
const setBranch = useCallback(
(path: string) => setBranchPath(path.split(">")),
[setBranchPath],
);
return {
get values() {
trackStreamMode("values");
@@ -631,17 +790,15 @@ export function useStream<
stop,
submit,
branch,
setBranch,
history: flatHistory,
experimental_branchTree: rootSequence,
get messages() {
trackStreamMode("messages-tuple");
if (getMessages == null) {
throw new Error(
"No messages key provided. Make sure that `useStream` contains the `messagesKey` property.",
);
}
return getMessages(values);
},
@@ -650,13 +807,6 @@ export function useStream<
index?: number,
): MessageMetadata<StateType> | undefined {
trackStreamMode("messages-tuple");
if (getMessages == null) {
throw new Error(
"No messages key provided. Make sure that `useStream` contains the `messagesKey` property.",
);
}
return messageMetadata?.find(
(m) => m.messageId === (message.id ?? index),
);
+5
View File
@@ -0,0 +1,5 @@
{
"pageTitleTemplates": {
"index": "{projectName}/react"
}
}
+57 -27
View File
@@ -301,6 +301,15 @@
resolved "https://registry.yarnpkg.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6"
integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==
"@gerrit0/mini-shiki@^1.24.0":
version "1.27.2"
resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz#cf2a9fcb08a6581c78fc94821f0c854ec4b9f899"
integrity sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==
dependencies:
"@shikijs/engine-oniguruma" "^1.27.2"
"@shikijs/types" "^1.27.2"
"@shikijs/vscode-textmate" "^10.0.1"
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -806,10 +815,26 @@
optionalDependencies:
fsevents "~2.3.2"
"@shikijs/core@1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.9.0.tgz#ff717fef5e0e9882f0848272699fd8f04d6f9a07"
integrity sha512-cbSoY8P/jgGByG8UOl3jnP/CWg/Qk+1q+eAKWtcrU3pNoILF8wTsLB0jT44qUBV8Ce1SvA9uqcM9Xf+u3fJFBw==
"@shikijs/engine-oniguruma@^1.27.2":
version "1.29.2"
resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz#d879717ced61d44e78feab16f701f6edd75434f1"
integrity sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==
dependencies:
"@shikijs/types" "1.29.2"
"@shikijs/vscode-textmate" "^10.0.1"
"@shikijs/types@1.29.2", "@shikijs/types@^1.27.2":
version "1.29.2"
resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.29.2.tgz#a93fdb410d1af8360c67bf5fc1d1a68d58e21c4f"
integrity sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==
dependencies:
"@shikijs/vscode-textmate" "^10.0.1"
"@types/hast" "^3.0.4"
"@shikijs/vscode-textmate@^10.0.1":
version "10.0.1"
resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz#d06d45b67ac5e9b0088e3f67ebd3f25c6c3d711a"
integrity sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==
"@sinclair/typebox@^0.27.8":
version "0.27.8"
@@ -910,6 +935,13 @@
dependencies:
"@types/node" "*"
"@types/hast@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
dependencies:
"@types/unist" "*"
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
@@ -996,6 +1028,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
"@types/unist@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
"@types/unist@^2", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
version "2.0.10"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc"
@@ -3271,7 +3308,7 @@ minimatch@^5.0.1:
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.3:
minimatch@^9.0.3, minimatch@^9.0.5:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
@@ -3857,13 +3894,6 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shiki@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.9.0.tgz#e4d3a044d9c746aefbea47615e83323fdc3dc361"
integrity sha512-i6//Lqgn7+7nZA0qVjoYH0085YdNk4MC+tJV4bo+HgjgRMJ0JmkLZzFAuvVioJqLkcGDK5GAMpghZEZkCnwxpQ==
dependencies:
"@shikijs/core" "1.9.0"
side-channel@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
@@ -4231,21 +4261,21 @@ typedarray.prototype.slice@^1.0.3:
typed-array-buffer "^1.0.2"
typed-array-byte-offset "^1.0.2"
typedoc-plugin-markdown@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.1.0.tgz#0969e82d9821c956145a4b8a9a70f4e00bde27e8"
integrity sha512-sUiEJVaa6+MOFShRy14j1OP/VXC5OLyHNecJ2nKeGuBy2M3YiMatSLoIiddFAqVptSuILJTZiJzCBIY6yzAVyg==
typedoc-plugin-markdown@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.4.2.tgz#fc31779595aa9bf00e66709f3894e048345bf7ed"
integrity sha512-kJVkU2Wd+AXQpyL6DlYXXRrfNrHrEIUgiABWH8Z+2Lz5Sq6an4dQ/hfvP75bbokjNDUskOdFlEEm/0fSVyC7eg==
typedoc@^0.26.1:
version "0.26.1"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.26.1.tgz#fc43108abdea64929a2e636877e250d5dea50957"
integrity sha512-APsVXqh93jTlpkLuw6+/IORx7n5LN8hzJV8nvMIrYYaIva0VCq0CoDN7Z3hsRThEYVExI/qoFHnAAxrhG+Wd7Q==
typedoc@^0.27.7:
version "0.27.7"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.27.7.tgz#09047ffb5c845f45765de26c68b77260867fe967"
integrity sha512-K/JaUPX18+61W3VXek1cWC5gwmuLvYTOXJzBvD9W7jFvbPnefRnCHQCEPw7MSNrP/Hj7JJrhZtDDLKdcYm6ucg==
dependencies:
"@gerrit0/mini-shiki" "^1.24.0"
lunr "^2.3.9"
markdown-it "^14.1.0"
minimatch "^9.0.4"
shiki "^1.9.0"
yaml "^2.4.5"
minimatch "^9.0.5"
yaml "^2.6.1"
typescript@^5.4.5:
version "5.4.5"
@@ -4468,10 +4498,10 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^2.4.5:
version "2.4.5"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e"
integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==
yaml@^2.6.1:
version "2.7.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98"
integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==
yargs-parser@^20.2.3:
version "20.2.9"
+2 -1
View File
@@ -1,5 +1,6 @@
from langgraph_sdk.auth import Auth
from langgraph_sdk.client import get_client, get_sync_client
from langgraph_sdk.routing import Middleware, Router
try:
from importlib import metadata
@@ -8,4 +9,4 @@ try:
except metadata.PackageNotFoundError:
__version__ = "unknown"
__all__ = ["Auth", "get_client", "get_sync_client"]
__all__ = ["Auth", "get_client", "get_sync_client", "Router", "Middleware"]
@@ -0,0 +1,111 @@
import typing
from langgraph_sdk.routing import types
from langgraph_sdk.routing.types import Middleware
@typing.final
class Router:
"""Add routes, middleware, and manage application lifecycle.
Define custom routes, apply middleware globally, and handle application startup/shutdown.
Middleware runs on all routes (including default LangGraph endpoints like /runs/, /assistants/, etc).
Custom routes take precedence over default ones, so you can override default behavior if needed.
???+ example "Basic Usage"
```python
from contextvars import ContextVar
from typing import Any
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from starlette.routing import Route
from langgraph_sdk import Router, Middleware
# Enterprise authentication middleware example
class JWTAuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Any, call_next: Any) -> Any:
try:
auth_header = request.headers["Authorization"]
token = auth_header.split("Bearer ")[1]
# Verify JWT token here
# Set user context for downstream handlers
request.state.user = {"id": "user_123"}
response = await call_next(request)
return response
except Exception:
return JSONResponse(
{"error": "Invalid or missing authentication"},
status_code=401
)
# Database connection pool in lifespan
async def db_lifespan(app):
# Initialize database connection pool
from databases import Database
database = Database("postgresql://user:pass@localhost/dbname")
await database.connect()
yield
await database.disconnect()
# Custom login endpoint
async def login(request):
data = await request.json()
# Verify credentials and generate JWT
return JSONResponse({
"token": "generated.jwt.token"
})
# Protected endpoint example
async def protected_route(request):
user = request.state.user
return JSONResponse({
"message": f"Hello {user['id']}"
})
router = Router(
middleware=[Middleware(JWTAuthMiddleware)],
lifespan=db_lifespan,
routes=[
Route("/auth/login", endpoint=login, methods=["POST"]),
Route("/api/protected", endpoint=protected_route, methods=["GET"])
]
)
???+ note "Request Processing Flow"
1. Middleware is applied in the order specified, wrapping all routes
2. Routes are matched in the following order:
* Custom routes defined in the Router take precedence
* Default LangGraph routes are used as fallback
3. Lifespan manages application startup/shutdown:
* Runs before any requests are processed
* Ideal for initializing shared resources (DB pools, caches, etc.)
* Cleanup occurs during application shutdown
This allows you to maintain enterprise-grade features while leveraging
LangGraph's built-in capabilities.
"""
__slots__ = ("routes", "lifespan", "middleware")
def __init__(
self,
routes: list[types.BaseRoute],
*,
lifespan: typing.Union[types.Lifespan[typing.Any], None] = None,
middleware: typing.Union[
list[
typing.Union[
types.Middleware,
tuple[types._MiddlewareFactory, typing.Any, typing.Any],
]
],
None,
] = None,
) -> None:
self.routes = routes
self.lifespan = lifespan
self.middleware: list[types.Middleware[typing.Any]] = middleware or []
__all__ = ["Router", "Middleware"]
@@ -0,0 +1,82 @@
"""Type-hints for the langgraph router.
Copied from Starlette. When implementing, use Starlette types."""
import enum
import typing
from typing_extensions import ParamSpec
class Match(enum.Enum):
NONE = 0
PARTIAL = 1
FULL = 2
AppType = typing.TypeVar("AppType")
Scope = typing.MutableMapping[str, typing.Any]
Message = typing.MutableMapping[str, typing.Any]
Receive = typing.Callable[[], typing.Awaitable[Message]]
Send = typing.Callable[[Message], typing.Awaitable[None]]
@typing.runtime_checkable
class BaseRoute(typing.Protocol):
def matches(self, scope: Scope) -> tuple[Match, Scope]:
"""Determine if the route matches the given scope."""
...
def url_path_for(self, name: str, /, **path_params: typing.Any) -> str:
"""Return the URL path for the given name and path parameters."""
...
async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
"""Handle the event."""
...
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
"""Handle the event."""
...
StatelessLifespan = typing.Callable[[AppType], typing.AsyncContextManager[None]]
StatefulLifespan = typing.Callable[
[AppType], typing.AsyncContextManager[typing.Mapping[str, typing.Any]]
]
Lifespan = typing.Union[StatelessLifespan[AppType], StatefulLifespan[AppType]]
ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]
P = ParamSpec("P")
class _MiddlewareFactory(typing.Protocol[P]):
def __call__(
self, app: ASGIApp, /, *args: P.args, **kwargs: P.kwargs
) -> ASGIApp: ... # pragma: no cover
# Copied from Starlette. Basically a named tuple
class Middleware:
def __init__(
self,
cls: _MiddlewareFactory[P],
*args: P.args,
**kwargs: P.kwargs,
) -> None:
self.cls = cls
self.args = args
self.kwargs = kwargs
def __iter__(self) -> typing.Iterator[typing.Any]:
as_tuple = (self.cls, self.args, self.kwargs)
return iter(as_tuple)
def __repr__(self) -> str:
class_name = self.__class__.__name__
args_strings = [f"{value!r}" for value in self.args]
option_strings = [f"{key}={value!r}" for key, value in self.kwargs.items()]
name = getattr(self.cls, "__name__", "")
args_repr = ", ".join([name] + args_strings + option_strings)
return f"{class_name}({args_repr})"
+127 -123
View File
@@ -2,13 +2,13 @@
[[package]]
name = "anyio"
version = "4.7.0"
version = "4.8.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.9"
files = [
{file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"},
{file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"},
{file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"},
{file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"},
]
[package.dependencies]
@@ -19,29 +19,29 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras]
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
trio = ["trio (>=0.26.1)"]
[[package]]
name = "certifi"
version = "2024.8.30"
version = "2025.1.31"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
]
[[package]]
name = "codespell"
version = "2.3.0"
description = "Codespell"
version = "2.4.1"
description = "Fix common misspellings in text files"
optional = false
python-versions = ">=3.8"
files = [
{file = "codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1"},
{file = "codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f"},
{file = "codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425"},
{file = "codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5"},
]
[package.extras]
@@ -168,49 +168,49 @@ files = [
[[package]]
name = "mypy"
version = "1.13.0"
version = "1.15.0"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
{file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
{file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
{file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
{file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
{file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
{file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
{file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
{file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
{file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
{file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
{file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
{file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
{file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
{file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
{file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
{file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
{file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
{file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
{file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
{file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
{file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
{file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
{file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
{file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
{file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
{file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
{file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
{file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
{file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
{file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
{file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
{file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"},
{file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"},
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"},
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"},
{file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"},
{file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"},
{file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"},
{file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"},
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"},
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"},
{file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"},
{file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"},
{file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"},
{file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"},
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"},
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"},
{file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"},
{file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"},
{file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"},
{file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"},
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"},
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"},
{file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"},
{file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"},
{file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"},
{file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"},
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"},
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"},
{file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"},
{file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"},
{file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"},
{file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"},
]
[package.dependencies]
mypy-extensions = ">=1.0.0"
mypy_extensions = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=4.6.0"
typing_extensions = ">=4.6.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
@@ -232,86 +232,90 @@ files = [
[[package]]
name = "orjson"
version = "3.10.12"
version = "3.10.15"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
files = [
{file = "orjson-3.10.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ece01a7ec71d9940cc654c482907a6b65df27251255097629d0dea781f255c6d"},
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34ec9aebc04f11f4b978dd6caf697a2df2dd9b47d35aa4cc606cabcb9df69d7"},
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6ec8658da3480939c79b9e9e27e0db31dffcd4ba69c334e98c9976ac29140e"},
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17e6baf4cf01534c9de8a16c0c611f3d94925d1701bf5f4aff17003677d8ced"},
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6402ebb74a14ef96f94a868569f5dccf70d791de49feb73180eb3c6fda2ade56"},
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0000758ae7c7853e0a4a6063f534c61656ebff644391e1f81698c1b2d2fc8cd2"},
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:888442dcee99fd1e5bd37a4abb94930915ca6af4db50e23e746cdf4d1e63db13"},
{file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1f7a3ce79246aa0e92f5458d86c54f257fb5dfdc14a192651ba7ec2c00f8a05"},
{file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:802a3935f45605c66fb4a586488a38af63cb37aaad1c1d94c982c40dcc452e85"},
{file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1da1ef0113a2be19bb6c557fb0ec2d79c92ebd2fed4cfb1b26bab93f021fb885"},
{file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a3273e99f367f137d5b3fecb5e9f45bcdbfac2a8b2f32fbc72129bbd48789c2"},
{file = "orjson-3.10.12-cp310-none-win32.whl", hash = "sha256:475661bf249fd7907d9b0a2a2421b4e684355a77ceef85b8352439a9163418c3"},
{file = "orjson-3.10.12-cp310-none-win_amd64.whl", hash = "sha256:87251dc1fb2b9e5ab91ce65d8f4caf21910d99ba8fb24b49fd0c118b2362d509"},
{file = "orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74"},
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23"},
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252"},
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef"},
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252"},
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4"},
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae"},
{file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b"},
{file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da"},
{file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07"},
{file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd"},
{file = "orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79"},
{file = "orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8"},
{file = "orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d"},
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f"},
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70"},
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69"},
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9"},
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192"},
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559"},
{file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc"},
{file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f"},
{file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be"},
{file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c"},
{file = "orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708"},
{file = "orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb"},
{file = "orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543"},
{file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296"},
{file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e"},
{file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f"},
{file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e"},
{file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6"},
{file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e"},
{file = "orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc"},
{file = "orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825"},
{file = "orjson-3.10.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7d69af5b54617a5fac5c8e5ed0859eb798e2ce8913262eb522590239db6c6763"},
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ed119ea7d2953365724a7059231a44830eb6bbb0cfead33fcbc562f5fd8f935"},
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5fc1238ef197e7cad5c91415f524aaa51e004be5a9b35a1b8a84ade196f73f"},
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43509843990439b05f848539d6f6198d4ac86ff01dd024b2f9a795c0daeeab60"},
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f72e27a62041cfb37a3de512247ece9f240a561e6c8662276beaf4d53d406db4"},
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a904f9572092bb6742ab7c16c623f0cdccbad9eeb2d14d4aa06284867bddd31"},
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:855c0833999ed5dc62f64552db26f9be767434917d8348d77bacaab84f787d7b"},
{file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:897830244e2320f6184699f598df7fb9db9f5087d6f3f03666ae89d607e4f8ed"},
{file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0b32652eaa4a7539f6f04abc6243619c56f8530c53bf9b023e1269df5f7816dd"},
{file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:36b4aa31e0f6a1aeeb6f8377769ca5d125db000f05c20e54163aef1d3fe8e833"},
{file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5535163054d6cbf2796f93e4f0dbc800f61914c0e3c4ed8499cf6ece22b4a3da"},
{file = "orjson-3.10.12-cp38-none-win32.whl", hash = "sha256:90a5551f6f5a5fa07010bf3d0b4ca2de21adafbbc0af6cb700b63cd767266cb9"},
{file = "orjson-3.10.12-cp38-none-win_amd64.whl", hash = "sha256:703a2fb35a06cdd45adf5d733cf613cbc0cb3ae57643472b16bc22d325b5fb6c"},
{file = "orjson-3.10.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f29de3ef71a42a5822765def1febfb36e0859d33abf5c2ad240acad5c6a1b78d"},
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de365a42acc65d74953f05e4772c974dad6c51cfc13c3240899f534d611be967"},
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a5a0158648a67ff0004cb0df5df7dcc55bfc9ca154d9c01597a23ad54c8d0c"},
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c47ce6b8d90fe9646a25b6fb52284a14ff215c9595914af63a5933a49972ce36"},
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0eee4c2c5bfb5c1b47a5db80d2ac7aaa7e938956ae88089f098aff2c0f35d5d8"},
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d3081bbe8b86587eb5c98a73b97f13d8f9fea685cf91a579beddacc0d10566"},
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c23a6e90383884068bc2dba83d5222c9fcc3b99a0ed2411d38150734236755"},
{file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5472be7dc3269b4b52acba1433dac239215366f89dc1d8d0e64029abac4e714e"},
{file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7319cda750fca96ae5973efb31b17d97a5c5225ae0bc79bf5bf84df9e1ec2ab6"},
{file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:74d5ca5a255bf20b8def6a2b96b1e18ad37b4a122d59b154c458ee9494377f80"},
{file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ff31d22ecc5fb85ef62c7d4afe8301d10c558d00dd24274d4bbe464380d3cd69"},
{file = "orjson-3.10.12-cp39-none-win32.whl", hash = "sha256:c22c3ea6fba91d84fcb4cda30e64aff548fcf0c44c876e681f47d61d24b12e6b"},
{file = "orjson-3.10.12-cp39-none-win_amd64.whl", hash = "sha256:be604f60d45ace6b0b33dd990a66b4526f1a7a186ac411c942674625456ca548"},
{file = "orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff"},
{file = "orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e"},
{file = "orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab"},
{file = "orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806"},
{file = "orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c"},
{file = "orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e"},
{file = "orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e"},
{file = "orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a"},
{file = "orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665"},
{file = "orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa"},
{file = "orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825"},
{file = "orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890"},
{file = "orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf"},
{file = "orjson-3.10.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e8afd6200e12771467a1a44e5ad780614b86abb4b11862ec54861a82d677746"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9a18c500f19273e9e104cca8c1f0b40a6470bcccfc33afcc088045d0bf5ea6"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb00b7bfbdf5d34a13180e4805d76b4567025da19a197645ca746fc2fb536586"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33aedc3d903378e257047fee506f11e0833146ca3e57a1a1fb0ddb789876c1e1"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0099ae6aed5eb1fc84c9eb72b95505a3df4267e6962eb93cdd5af03be71c98"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c864a80a2d467d7786274fce0e4f93ef2a7ca4ff31f7fc5634225aaa4e9e98c"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c25774c9e88a3e0013d7d1a6c8056926b607a61edd423b50eb5c88fd7f2823ae"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e78c211d0074e783d824ce7bb85bf459f93a233eb67a5b5003498232ddfb0e8a"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:43e17289ffdbbac8f39243916c893d2ae41a2ea1a9cbb060a56a4d75286351ae"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:781d54657063f361e89714293c095f506c533582ee40a426cb6489c48a637b81"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6875210307d36c94873f553786a808af2788e362bd0cf4c8e66d976791e7b528"},
{file = "orjson-3.10.15-cp38-cp38-win32.whl", hash = "sha256:305b38b2b8f8083cc3d618927d7f424349afce5975b316d33075ef0f73576b60"},
{file = "orjson-3.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:5dd9ef1639878cc3efffed349543cbf9372bdbd79f478615a1c633fe4e4180d1"},
{file = "orjson-3.10.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ffe19f3e8d68111e8644d4f4e267a069ca427926855582ff01fc012496d19969"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d433bf32a363823863a96561a555227c18a522a8217a6f9400f00ddc70139ae2"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da03392674f59a95d03fa5fb9fe3a160b0511ad84b7a3914699ea5a1b3a38da2"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a63bb41559b05360ded9132032239e47983a39b151af1201f07ec9370715c82"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3766ac4702f8f795ff3fa067968e806b4344af257011858cc3d6d8721588b53f"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1c73dcc8fadbd7c55802d9aa093b36878d34a3b3222c41052ce6b0fc65f8e8"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b299383825eafe642cbab34be762ccff9fd3408d72726a6b2a4506d410a71ab3"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:abc7abecdbf67a173ef1316036ebbf54ce400ef2300b4e26a7b843bd446c2480"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3614ea508d522a621384c1d6639016a5a2e4f027f3e4a1c93a51867615d28829"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:295c70f9dc154307777ba30fe29ff15c1bcc9dfc5c48632f37d20a607e9ba85a"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:63309e3ff924c62404923c80b9e2048c1f74ba4b615e7584584389ada50ed428"},
{file = "orjson-3.10.15-cp39-cp39-win32.whl", hash = "sha256:a2f708c62d026fb5340788ba94a55c23df4e1869fec74be455e0b2f5363b8507"},
{file = "orjson-3.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:efcf6c735c3d22ef60c4aa27a5238f1a477df85e9b15f2142f9d669beb2d13fd"},
{file = "orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e"},
]
[[package]]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
version = "0.1.51"
version = "0.1.52"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"