mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-10 02:15:15 +02:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9f26d28ea | ||
|
|
06d4ba7fa0 | ||
|
|
2dbdb36743 | ||
|
|
2b72fbd5de | ||
|
|
c681545c97 | ||
|
|
5ebdefba63 | ||
|
|
441923282c |
@@ -56,6 +56,12 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: langchain-ai/langchainjs
|
||||
token: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
|
||||
path: docs/langchainjs
|
||||
|
||||
- name: Set up Python + Poetry ${{ env.POETRY_VERSION }}
|
||||
uses: "./.github/actions/poetry_setup"
|
||||
with:
|
||||
@@ -72,6 +78,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd langchainjs && yarn && yarn build && cd ..
|
||||
yarn
|
||||
poetry install --with test --with docs --no-root
|
||||
poetry run pip install -U \
|
||||
|
||||
+873
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.5.1.cjs
|
||||
+23
-3
@@ -13,7 +13,22 @@ build-prebuilt:
|
||||
poetry run python -m _scripts.third_party_page.get_download_stats stats.yml
|
||||
poetry run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/prebuilt.md --language python
|
||||
|
||||
build-docs: build-typedoc build-prebuilt
|
||||
grab-langgraphjs:
|
||||
if [ -d "langgraphjs" ]; then \
|
||||
if [ ! -d "langgraphjs/.git" ]; then \
|
||||
rm -rf langgraphjs; \
|
||||
fi \
|
||||
fi
|
||||
if [ ! -d "langgraphjs" ]; then \
|
||||
git clone https://github.com/langchain-ai/langgraphjs.git; \
|
||||
else \
|
||||
cd langgraphjs && git checkout main && git pull; \
|
||||
fi
|
||||
cd langgraphjs && yarn
|
||||
cd langgraphjs && yarn build
|
||||
yarn
|
||||
|
||||
build-docs: build-typedoc build-prebuilt grab-langgraphjs
|
||||
poetry run python -m mkdocs build --clean -f mkdocs.yml --strict
|
||||
|
||||
llms-text:
|
||||
@@ -26,9 +41,14 @@ install-vercel-deps:
|
||||
# don't use vercel's python - it wasn't compiled with sqlite support, and it fails when installing ipython's kernel
|
||||
poetry env use /usr/bin/python3.11
|
||||
poetry install --with docs --with test --no-root
|
||||
poetry run pip install "git+https://github.com/benjamincburns/markdown-exec.git@cc0d39d737e5ffd4b83d23cd8729d7ea16e363c8"
|
||||
poetry run python3 -m ipykernel install --name=python3
|
||||
npm install -g tslab
|
||||
poetry run tslab install --python=python3
|
||||
poetry run jupyter kernelspec list
|
||||
|
||||
tests:
|
||||
# Run unit tests
|
||||
# RUn unit tests
|
||||
poetry run pytest tests/unit_tests
|
||||
|
||||
|
||||
@@ -39,7 +59,7 @@ vercel-build-docs: install-vercel-deps
|
||||
serve-clean-docs: clean-docs
|
||||
poetry run python -m mkdocs serve -c -f mkdocs.yml --strict -w ../libs/langgraph
|
||||
|
||||
serve-docs: build-typedoc
|
||||
serve-docs: build-typedoc grab-langgraphjs
|
||||
poetry run python -m mkdocs serve -f mkdocs.yml -w ../libs/langgraph -w ../libs/checkpoint -w ../libs/sdk-py --dirty
|
||||
|
||||
clean-docs:
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import nock, { Definition } from "nock";
|
||||
import msgpack from "msgpack-lite";
|
||||
import zlib from "node:zlib";
|
||||
import fs from "node:fs/promises";
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
// deno style imports here because we're running this in the deno jupyter kernel
|
||||
|
||||
interface NockCassetteData {
|
||||
hash: string;
|
||||
entries: Definition[];
|
||||
}
|
||||
|
||||
// Utility functions for compression & serialization
|
||||
function compressData(data: NockCassetteData, compressionLevel = 9): string {
|
||||
const packed = msgpack.encode(data);
|
||||
const compressed = zlib.deflateSync(packed, { level: compressionLevel });
|
||||
return compressed.toString("base64");
|
||||
}
|
||||
|
||||
function decompressData(compressedString: string): NockCassetteData {
|
||||
const decoded = Buffer.from(compressedString, "base64");
|
||||
const decompressed = zlib.inflateSync(decoded);
|
||||
return msgpack.decode(decompressed) as NockCassetteData;
|
||||
}
|
||||
|
||||
class HashedCassette {
|
||||
hash: string;
|
||||
private recording = true;
|
||||
|
||||
constructor(
|
||||
private readonly cassettePath: string,
|
||||
hash: string
|
||||
) {
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
async enter() {
|
||||
try {
|
||||
const rawCassette = await fs.readFile(this.cassettePath, "utf-8");
|
||||
const data = decompressData(rawCassette);
|
||||
if (data.hash === this.hash) {
|
||||
this.recording = false;
|
||||
nock.disableNetConnect();
|
||||
nock.define(data.entries);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("ENOENT")) {
|
||||
this.recording = true;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
nock.recorder.rec({
|
||||
dont_print: true,
|
||||
output_objects: true,
|
||||
});
|
||||
}
|
||||
|
||||
async exit() {
|
||||
if (this.recording) {
|
||||
const entries = nock.recorder.play() as Definition[];
|
||||
const data = {
|
||||
hash: this.hash,
|
||||
entries,
|
||||
};
|
||||
const compressed = compressData(data);
|
||||
await fs.writeFile(this.cassettePath, compressed);
|
||||
} else {
|
||||
nock.enableNetConnect();
|
||||
nock.restore();
|
||||
nock.cleanAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import base64
|
||||
import os
|
||||
import zlib
|
||||
from types import TracebackType
|
||||
from typing import Optional, Any, Type
|
||||
|
||||
import msgpack
|
||||
import vcr
|
||||
|
||||
os.environ.pop("LANGCHAIN_TRACING_V2", None)
|
||||
custom_vcr = vcr.VCR()
|
||||
|
||||
|
||||
def compress_data(data: Any, compression_level: int = 9) -> str:
|
||||
packed = msgpack.packb(data, use_bin_type=True)
|
||||
compressed = zlib.compress(packed, level=compression_level)
|
||||
return base64.b64encode(compressed).decode("utf-8")
|
||||
|
||||
|
||||
def decompress_data(compressed_string: str) -> Any:
|
||||
decoded = base64.b64decode(compressed_string)
|
||||
decompressed = zlib.decompress(decoded)
|
||||
return msgpack.unpackb(decompressed, raw=False)
|
||||
|
||||
|
||||
class AdvancedCompressedSerializer:
|
||||
def serialize(self, cassette_dict: Any) -> str:
|
||||
return compress_data(cassette_dict)
|
||||
|
||||
def deserialize(self, cassette_string: str) -> Any:
|
||||
return decompress_data(cassette_string)
|
||||
|
||||
|
||||
custom_vcr.register_serializer("advanced_compressed", AdvancedCompressedSerializer())
|
||||
custom_vcr.serializer = "advanced_compressed"
|
||||
|
||||
|
||||
class HashedCassette:
|
||||
def __init__(self, cassette_path: str, hash_value: str) -> None:
|
||||
"""A context manager for using VCR cassettes with an embedded hash value.
|
||||
|
||||
Args:
|
||||
cassette_path (str): The file path of the cassette (independent of hash).
|
||||
hash_value (str): The expected hash value (e.g. a uuid string).
|
||||
|
||||
This class provides a context manager for using VCR cassettes with an embedded hash value.
|
||||
The hash value is used to ensure that the cassette matches the expected state, and if not,
|
||||
the cassette is removed or updated with the new hash value.
|
||||
"""
|
||||
self.cassette_path: str = cassette_path
|
||||
self.hash_value: str = hash_value
|
||||
self.vcr: vcr.VCR = custom_vcr
|
||||
self.cassette_context: Optional[Any] = None
|
||||
self.exited: bool = False
|
||||
|
||||
def __enter__(self) -> Any:
|
||||
self.exited: bool = False
|
||||
# Get the serializer instance from the VCR instance.
|
||||
serializer = self.vcr.serializers[self.vcr.serializer]
|
||||
# If the cassette file exists, check its embedded hash.
|
||||
if os.path.exists(self.cassette_path):
|
||||
with open(self.cassette_path, "r") as f:
|
||||
content = f.read()
|
||||
try:
|
||||
cassette_data = serializer.deserialize(content)
|
||||
except Exception:
|
||||
os.remove(self.cassette_path)
|
||||
else:
|
||||
existing_hash = cassette_data.get("cassette_hash")
|
||||
if existing_hash != self.hash_value:
|
||||
os.remove(self.cassette_path)
|
||||
# Now enter the VCR cassette context.
|
||||
self.cassette_context = custom_vcr.use_cassette(
|
||||
self.cassette_path,
|
||||
filter_headers=["x-api-key", "authorization"],
|
||||
record_mode="once",
|
||||
serializer="advanced_compressed",
|
||||
)
|
||||
return self.cassette_context.__enter__()
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]] = None,
|
||||
exc_val: Optional[BaseException] = None,
|
||||
exc_tb: Optional[TracebackType] = None,
|
||||
) -> Optional[bool]:
|
||||
if self.exited:
|
||||
return
|
||||
self.exited = True
|
||||
# Exit the VCR cassette context.
|
||||
result = self.cassette_context.__exit__(exc_type, exc_val, exc_tb)
|
||||
serializer = self.vcr.serializers[self.vcr.serializer]
|
||||
# If a cassette was recorded (or updated), open and update its hash.
|
||||
if os.path.exists(self.cassette_path):
|
||||
with open(self.cassette_path, "r") as f:
|
||||
content = f.read()
|
||||
try:
|
||||
cassette_data = serializer.deserialize(content)
|
||||
except Exception:
|
||||
return result
|
||||
# Update the cassette data with the expected hash.
|
||||
if cassette_data.get("cassette_hash") != self.hash_value:
|
||||
cassette_data["cassette_hash"] = self.hash_value
|
||||
serialized_data = serializer.serialize(cassette_data)
|
||||
with open(self.cassette_path, "w") as f:
|
||||
f.write(serialized_data)
|
||||
return result
|
||||
@@ -0,0 +1,4 @@
|
||||
hook_state = {
|
||||
"document_filename": "__UNKNOWN__",
|
||||
"document_content": "__UNKNOWN__",
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import argparse
|
||||
import ast
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
from typing import Literal, Optional
|
||||
|
||||
import nbformat
|
||||
from nbconvert.exporters import MarkdownExporter
|
||||
@@ -350,6 +352,17 @@ exporter = MarkdownExporter(
|
||||
],
|
||||
)
|
||||
|
||||
md_executable = MarkdownExporter(
|
||||
preprocessors=[
|
||||
ExtractAttachmentsPreprocessor,
|
||||
EscapePreprocessor(markdown_exec_migration=True),
|
||||
],
|
||||
template_name="md_executable",
|
||||
extra_template_basedirs=[
|
||||
os.path.join(os.path.dirname(__file__), "notebook_convert_templates")
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def convert_notebook(
|
||||
notebook_path: Path,
|
||||
@@ -359,5 +372,99 @@ def convert_notebook(
|
||||
nb = nbformat.read(f, as_version=4)
|
||||
|
||||
nb.metadata.mode = mode
|
||||
body, _ = exporter.from_notebook_node(nb)
|
||||
if mode == "markdown":
|
||||
body, _ = exporter.from_notebook_node(nb)
|
||||
else:
|
||||
body, _ = md_executable.from_notebook_node(nb)
|
||||
return body
|
||||
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
DOCS = HERE.parent / "docs"
|
||||
|
||||
|
||||
# Convert notebooks to markdown
|
||||
def _convert_notebooks(
|
||||
*,
|
||||
output_dir: Optional[Path] = None,
|
||||
replace: bool = False,
|
||||
pattern: str = "*.ipynb",
|
||||
) -> None:
|
||||
"""Converting notebooks."""
|
||||
if not output_dir and not replace:
|
||||
raise ValueError("Either --output_dir or --replace must be specified")
|
||||
|
||||
output_dir_path = DOCS if replace else Path(output_dir)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
with open(markdown_path, "w") as f:
|
||||
f.write(markdown)
|
||||
if replace:
|
||||
notebook.unlink(missing_ok=False)
|
||||
|
||||
if replace:
|
||||
# The regex will match markdown links that point to *.ipynb files.
|
||||
# It captures:
|
||||
# group(1): the link text (inside the square brackets)
|
||||
# group(2): the file path (without the trailing .ipynb)
|
||||
link_pattern = r"(?<!!)\[([^\]]+)\]\((?![^)]*//)([^)]+)\.ipynb\)"
|
||||
|
||||
def replace_link(match: re.Match) -> str:
|
||||
link_text = match.group(1)
|
||||
link_target = match.group(2)
|
||||
# Reconstruct the file name with the .ipynb extension.
|
||||
# For example, if link_target is "foo/bar", then linked_file becomes "bar.ipynb".
|
||||
linked_file = Path(link_target).name + ".ipynb"
|
||||
# Only update if the notebook was among those converted.
|
||||
if linked_file in file_names:
|
||||
# Change the extension from .ipynb to .md
|
||||
return f"[{link_text}]({link_target}.md)"
|
||||
# Otherwise, leave the original link intact.
|
||||
return match.group(0)
|
||||
|
||||
# Process all markdown files in the output directory.
|
||||
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)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(new_content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Convert notebooks to markdown")
|
||||
parser.add_argument(
|
||||
"--output_dir",
|
||||
default=None,
|
||||
help="Directory to output markdown files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--replace",
|
||||
action="store_true",
|
||||
help="Replace original notebooks with markdown files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pattern",
|
||||
default="*.ipynb",
|
||||
help="Glob pattern to match notebooks to convert",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
_convert_notebooks(
|
||||
replace=args.replace,
|
||||
output_dir=args.output_dir,
|
||||
pattern=args.pattern,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"mimetypes": {
|
||||
"text/markdown": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{#https://github.com/rdbisme/nbconvert/blob/master/share/jupyter/nbconvert/templates/markdown/index.md.j2#}
|
||||
{% extends 'markdown/index.md.j2' %}
|
||||
|
||||
{% block input %}
|
||||
```
|
||||
{%- 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 }}{% 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}}
|
||||
```
|
||||
{% endblock input %}
|
||||
|
||||
{%- block traceback_line -%}
|
||||
{%- endblock traceback_line -%}
|
||||
|
||||
{%- block stream -%}
|
||||
{%- endblock stream -%}
|
||||
|
||||
{%- block data_text scoped -%}
|
||||
{%- endblock data_text -%}
|
||||
|
||||
{%- block data_html scoped -%}
|
||||
```html
|
||||
{{ output.data['text/html'] | safe }}
|
||||
```
|
||||
{%- endblock data_html -%}
|
||||
|
||||
{%- block data_jpg scoped -%}
|
||||

|
||||
{%- endblock data_jpg -%}
|
||||
|
||||
{%- block data_png scoped -%}
|
||||

|
||||
{%- endblock data_png -%}
|
||||
+121
-29
@@ -2,13 +2,21 @@ import logging
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
import traceback
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
from markdown import Markdown
|
||||
from mkdocs.structure.files import Files, File
|
||||
from mkdocs.structure.pages import Page
|
||||
from pymdownx.superfences import SuperFencesException
|
||||
|
||||
from _scripts.hook_state import hook_state
|
||||
from markdown_exec.hooks import SessionHistoryEntry
|
||||
|
||||
|
||||
from _scripts.generate_api_reference_links import update_markdown_with_imports
|
||||
from _scripts.notebook_convert import convert_notebook
|
||||
from _scripts.setup_vcr import get_hash_for_session, load_postamble, load_preamble, _hash_string
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
@@ -57,29 +65,6 @@ def on_files(files: Files, **kwargs: Dict[str, Any]):
|
||||
return new_files
|
||||
|
||||
|
||||
def _add_path_to_code_blocks(markdown: str, page: Page) -> str:
|
||||
"""Add the path to the code blocks."""
|
||||
code_block_pattern = re.compile(
|
||||
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
|
||||
)
|
||||
|
||||
def replace_code_block_header(match: re.Match) -> str:
|
||||
indent = match.group("indent")
|
||||
language = match.group("language")
|
||||
attributes = match.group("attributes").rstrip()
|
||||
|
||||
if 'exec="on"' not in attributes:
|
||||
# Return original code block
|
||||
return match.group(0)
|
||||
|
||||
code = match.group("code")
|
||||
return f'{indent}```{language} {attributes} path="{page.file.src_path}"\n{code}{indent}```'
|
||||
|
||||
return code_block_pattern.sub(replace_code_block_header, markdown)
|
||||
|
||||
|
||||
def _highlight_code_blocks(markdown: str) -> str:
|
||||
"""Find code blocks with highlight comments and add hl_lines attribute.
|
||||
|
||||
@@ -157,6 +142,110 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
markdown = code_block_pattern.sub(replace_highlight_comments, markdown)
|
||||
return markdown
|
||||
|
||||
def handle_vcr_setup(
|
||||
*,
|
||||
formatter: Callable,
|
||||
language: str,
|
||||
code: str,
|
||||
session: str,
|
||||
id: str,
|
||||
md: Markdown,
|
||||
**kwargs: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle VCR setup in markdown content if necessary."""
|
||||
logger.info(f"handle_vcr_setup: {hook_state['document_filename']}")
|
||||
try:
|
||||
if hook_state['document_filename'] == '__UNKNOWN__':
|
||||
raise SuperFencesException(
|
||||
f"error while processing {language} block: document filename hasn't been set yet"
|
||||
)
|
||||
|
||||
if hook_state['document_content'] == '__UNKNOWN__':
|
||||
raise SuperFencesException(
|
||||
f"error while processing {language} block: document content hasn't been set yet"
|
||||
)
|
||||
|
||||
if session is None or session == "" and id is None or id == "":
|
||||
id = _hash_string(code)
|
||||
|
||||
if session is not None and session != "":
|
||||
logger.info(f"new {language} session {session} on page {hook_state['document_filename']}")
|
||||
|
||||
cassette_prefix = hook_state['document_filename'].replace(".md", "").replace(os.path.sep, "_")
|
||||
|
||||
cassette_dir = os.path.abspath(
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), "cassettes")
|
||||
)
|
||||
os.makedirs(cassette_dir, exist_ok=True)
|
||||
|
||||
# Build a unique cassette name.
|
||||
cassette_name = os.path.join(
|
||||
cassette_dir,
|
||||
f"{cassette_prefix}_{session if session else id}_{language}.msgpack.zlib",
|
||||
)
|
||||
|
||||
# Add context manager at start with explicit __enter__ and __exit__ calls
|
||||
|
||||
hash_ = get_hash_for_session(language, session, hook_state['document_content'])
|
||||
wrapped_lines = [
|
||||
load_preamble(language, hash_, cassette_name),
|
||||
code,
|
||||
]
|
||||
|
||||
if session is None or session == "":
|
||||
logger.info(
|
||||
f"no session, adding postamble for {language} in {hook_state['document_filename']}"
|
||||
)
|
||||
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={ **keep_extras, "path": hook_state['document_filename'] },
|
||||
)
|
||||
except Exception as e:
|
||||
raise SuperFencesException(traceback.format_exc()) from e
|
||||
|
||||
|
||||
def handle_vcr_teardown(
|
||||
*,
|
||||
formatter: Callable,
|
||||
language: str,
|
||||
session: str,
|
||||
history: list[SessionHistoryEntry],
|
||||
):
|
||||
code = load_postamble(language)
|
||||
html = False
|
||||
update_toc = False
|
||||
|
||||
logger.info(f"tearing down {language} {session} on {hook_state['document_filename']}")
|
||||
|
||||
kwargs = dict(
|
||||
code=code,
|
||||
session=session,
|
||||
id=f"{id}_vcr_end",
|
||||
md=None, # md is unused by the formatter, but it's a required argument
|
||||
html=html,
|
||||
update_toc=update_toc,
|
||||
extra={},
|
||||
)
|
||||
|
||||
# This doesn't actually render anything, we just call the formatter so it
|
||||
# executes in the same context as the session of which we're disposing.
|
||||
formatter(**kwargs)
|
||||
|
||||
|
||||
def _on_page_markdown_with_config(
|
||||
markdown: str,
|
||||
@@ -179,11 +268,6 @@ def _on_page_markdown_with_config(
|
||||
# Apply highlight comments to code blocks
|
||||
markdown = _highlight_code_blocks(markdown)
|
||||
|
||||
# Add file path as an attribute to code blocks that are executable.
|
||||
# This file path is used to associate fixtures with the executable code
|
||||
# which can be used in CI to test the docs without making network requests.
|
||||
markdown = _add_path_to_code_blocks(markdown, page)
|
||||
|
||||
if remove_base64_images:
|
||||
# Remove base64 encoded images from markdown
|
||||
markdown = re.sub(r"!\[.*?\]\(data:image/+;base64,[^\)]+\)", "", markdown)
|
||||
@@ -192,6 +276,8 @@ def _on_page_markdown_with_config(
|
||||
|
||||
|
||||
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
logger.info(f"on_page_markdown: {page.file.src_path}")
|
||||
hook_state['document_filename'] = page.file.src_path
|
||||
return _on_page_markdown_with_config(
|
||||
markdown,
|
||||
page,
|
||||
@@ -253,3 +339,9 @@ def on_post_build(config):
|
||||
+ suffix
|
||||
)
|
||||
write_html(config["site_dir"], old_html_path, new_html_path)
|
||||
|
||||
def on_pre_page(page: Page, **kwargs: Dict[str, Any]):
|
||||
logger.info(f"on_pre_page: {page.file.src_path}")
|
||||
hook_state['document_filename'] = page.file.src_path
|
||||
hook_state['document_content'] = page.file.content_string
|
||||
return page
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# A list of patterns that, if found in a code block, will cause us to leave that block unchanged.
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from textwrap import dedent, indent
|
||||
|
||||
from mistune import BlockParser, BlockState, Markdown, create_markdown
|
||||
from mistune.renderers.markdown import MarkdownRenderer
|
||||
|
||||
preambles = {
|
||||
"python": "vcr_setup_preamble.py",
|
||||
"typescript": "nock_setup_preamble.ts",
|
||||
}
|
||||
|
||||
|
||||
def _get_python_cassette_init(cassette_name: str, hash_: str) -> str:
|
||||
return dedent(
|
||||
f"""
|
||||
_cassette = HashedCassette('{cassette_name}', '{hash_}')
|
||||
_cassette.__enter__()
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _get_typescript_cassette_init(cassette_name: str, hash_: str) -> str:
|
||||
return dedent(
|
||||
f"""
|
||||
const _cassette = new HashedCassette("{cassette_name}", "{hash_}");
|
||||
await _cassette.enter();
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _get_python_cassette_cleanup() -> str:
|
||||
return "_cassette.__exit__()"
|
||||
|
||||
|
||||
def _get_typescript_cassette_cleanup() -> str:
|
||||
return "await _cassette.exit();"
|
||||
|
||||
|
||||
preamble_inits = {
|
||||
"python": _get_python_cassette_init,
|
||||
"py": _get_python_cassette_init,
|
||||
"typescript": _get_typescript_cassette_init,
|
||||
"ts": _get_typescript_cassette_init,
|
||||
}
|
||||
|
||||
preamble_cleanups = {
|
||||
"python": _get_python_cassette_cleanup,
|
||||
"py": _get_python_cassette_cleanup,
|
||||
"typescript": _get_typescript_cassette_cleanup,
|
||||
"ts": _get_typescript_cassette_cleanup,
|
||||
}
|
||||
|
||||
|
||||
def load_preamble(language: str, hash_: str, cassette_name: str) -> str:
|
||||
"""Load the source code for the preamble for a given language."""
|
||||
_assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
|
||||
|
||||
preamble_path = os.path.join(_assets_dir, preambles[language])
|
||||
with open(preamble_path, "r") as f:
|
||||
lines = f.readlines()
|
||||
lines.append(preamble_inits[language](cassette_name, hash_))
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
def load_postamble(language: str) -> str:
|
||||
"""Load the source code for the postamble for a given language."""
|
||||
return preamble_cleanups[language]()
|
||||
|
||||
|
||||
def _hash_string(input_string: str) -> str:
|
||||
# Encode the input string to bytes
|
||||
encoded_string = input_string.encode("utf-8")
|
||||
# Create a SHA-256 hash object
|
||||
sha256_hash = hashlib.sha256(encoded_string)
|
||||
# Get the hexadecimal digest of the hash
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
|
||||
def extract_code_blocks_for_session(language: str, session: str, content: str) -> str:
|
||||
code_blocks_for_session = []
|
||||
|
||||
TAB_REGEX = r"^===!? \"(?P<title>[^\"]+)\"\n(?P<content>(?:(?P<indent> )+[^\n]*\n)+)"
|
||||
def parse_tabs(block: BlockParser, m: re.Match, state: BlockState) -> str:
|
||||
state.append_token(
|
||||
{
|
||||
"raw": m.group(0),
|
||||
"type": "block_tab",
|
||||
"attrs": {
|
||||
"title": m.group("title"),
|
||||
"level": len(m.group("indent")) // 4,
|
||||
"content": dedent(m.group("content")).strip(),
|
||||
},
|
||||
}
|
||||
)
|
||||
return m.end()
|
||||
|
||||
def render_tabs(self, token: dict, state: BlockState):
|
||||
recursive_transformer = create_markdown(renderer=DocumentRenderer())
|
||||
recursive_transformer.block.register("block_tab", TAB_REGEX, parse_tabs, before='list')
|
||||
recursive_transformer.renderer.register("block_tab", render_tabs)
|
||||
return (
|
||||
f'=== "{token["attrs"]["title"]}"\n'
|
||||
f'{indent(recursive_transformer(token["attrs"]["content"]), " " * token["attrs"]["level"])}\n'
|
||||
)
|
||||
|
||||
class DocumentRenderer(MarkdownRenderer):
|
||||
def block_code(self, token: dict, state: BlockState):
|
||||
if token["style"] == "fenced":
|
||||
if token["attrs"]["info"]:
|
||||
attributes = {}
|
||||
block_language = token["attrs"]["info"].split()[0]
|
||||
for match in re.finditer(r'(?P<key>\w+)=(?:(?P<value>(?:[\w]+))|"(?P<value_quoted>(?:[^"\s]+))")', token["attrs"]["info"]):
|
||||
attributes[match.group("key")] = match.group("value") or match.group("value_quoted")
|
||||
if block_language == language and "session" in attributes and attributes["session"] == session:
|
||||
code_blocks_for_session.append(token["raw"].rstrip())
|
||||
return super().block_code(token, state)
|
||||
|
||||
transformer: Markdown = create_markdown(renderer=DocumentRenderer())
|
||||
transformer.block.register("block_tab", TAB_REGEX, parse_tabs, before='list')
|
||||
transformer.renderer.register("block_tab", render_tabs)
|
||||
|
||||
# Parses the page content, which causes the code blocks to be added to the code_blocks_for_session list.
|
||||
# There's probably some way to do this by using the renderer as a filter, but I would've had to NO-OP
|
||||
# all of the default behavior, and this was easier.
|
||||
transformer(content)
|
||||
|
||||
return code_blocks_for_session
|
||||
|
||||
|
||||
def get_hash_for_session(language: str, session: str, content: str) -> str:
|
||||
# include the preamble in the hash so we invalidate if it changes
|
||||
preamble_hash = _hash_string(load_preamble(language, session, "test"))
|
||||
|
||||
code_blocks_for_session = [preamble_hash, *extract_code_blocks_for_session(language, session, content)]
|
||||
|
||||
return _hash_string("\n".join(code_blocks_for_session))
|
||||
@@ -89,7 +89,7 @@ def generate_markdown(resolved_packages: List[ResolvedPackage], language: str) -
|
||||
for package in sorted_packages:
|
||||
name = f"**{package['name']}**"
|
||||
repo_url = f"[{package['repo']}](https://github.com/{package['repo']})"
|
||||
downloads = package["weekly_downloads"] or "-"
|
||||
downloads = package["weekly_downloads"] or 0
|
||||
row = f"| {name} | {repo_url} | {package['description']} | {downloads} |"
|
||||
rows.append(row)
|
||||
markdown_content = MARKDOWN.format(
|
||||
|
||||
@@ -35,45 +35,20 @@ def _get_weekly_downloads(packages: list[Package]) -> list[ResolvedPackage]:
|
||||
resolved_packages: list[ResolvedPackage] = []
|
||||
|
||||
for package in packages:
|
||||
# First check if package exists on PyPI
|
||||
pypi_url = f"https://pypi.org/pypi/{package['name']}/json"
|
||||
try:
|
||||
pypi_response = requests.get(pypi_url)
|
||||
pypi_response.raise_for_status()
|
||||
except requests.exceptions.HTTPError:
|
||||
raise AssertionError(f"Package {package['name']} does not exist on PyPI")
|
||||
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
|
||||
|
||||
# Get first release date
|
||||
pypi_data = pypi_response.json()
|
||||
releases = pypi_data["releases"]
|
||||
first_release_date = None
|
||||
for version_releases in releases.values():
|
||||
if version_releases: # Some versions may be empty lists
|
||||
upload_time = datetime.fromisoformat(version_releases[0]["upload_time"])
|
||||
if first_release_date is None or upload_time < first_release_date:
|
||||
first_release_date = upload_time
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if first_release_date is None:
|
||||
raise AssertionError(f"Package {package['name']} has no releases yet")
|
||||
sorted_data = sorted(
|
||||
data["data"],
|
||||
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# If package was published in last 48 hours, skip download stats
|
||||
if (datetime.now() - first_release_date).total_seconds() >= 48 * 3600:
|
||||
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
|
||||
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
sorted_data = sorted(
|
||||
data["data"],
|
||||
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# Sum the last 7 days of downloads
|
||||
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
|
||||
else:
|
||||
num_downloads = None
|
||||
# Sum the last 7 days of downloads
|
||||
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
|
||||
|
||||
resolved_packages.append(
|
||||
{
|
||||
|
||||
@@ -9,9 +9,3 @@ packages:
|
||||
- name: "langgraph-supervisor"
|
||||
repo: "langchain-ai/langgraph-supervisor"
|
||||
description: "Build supervisor multi-agent systems with LangGraph"
|
||||
- name: "langmem"
|
||||
repo: "langchain-ai/langmem"
|
||||
description: "Build agents that learn and adapt from interactions over time."
|
||||
- name: "langchain-mcp-adapters"
|
||||
repo: "langchain-ai/langchain-mcp-adapters"
|
||||
description: "Make Anthropic Model Context Protocol (MCP) tools compatible with LangGraph agents."
|
||||
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
@@ -15,7 +15,7 @@ Key features:
|
||||
|
||||
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).
|
||||
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://github.com/langchain-ai/assistant-ui).
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Functional API
|
||||
|
||||
!!! warning "Beta"
|
||||
The Functional API is currently in **beta** and is subject to change. Please [report any issues](https://github.com/langchain-ai/langgraph/issues) or feedback to the LangGraph team.
|
||||
|
||||
## Overview
|
||||
|
||||
The **Functional API** allows you to add LangGraph's key features -- [persistence](./persistence.md), [memory](./memory.md), [human-in-the-loop](./human_in_the_loop.md), and [streaming](./streaming.md) — to your applications with minimal changes to your existing code.
|
||||
|
||||
@@ -28,7 +28,7 @@ The conceptual guide does not cover step-by-step instructions or specific implem
|
||||
- [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.
|
||||
- [Functional API](functional_api.md): `@entrypoint` and `@task` decorators that allow you to add LangGraph functionality to an existing codebase.
|
||||
- [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.
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
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
@@ -11,9 +11,9 @@ Here you’ll 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)
|
||||
|
||||
@@ -39,7 +39,8 @@ execution of your graph.
|
||||
- [How to use MongoDB checkpointer for persistence](persistence_mongodb.ipynb)
|
||||
- [How to create a custom checkpointer using Redis](persistence_redis.ipynb)
|
||||
|
||||
See the below guides for how-to add persistence to your workflow using the [Functional API](../concepts/functional_api.md):
|
||||
See the below guides for how-to add persistence to your workflow using the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to add thread-level persistence (functional API)](persistence-functional.ipynb)
|
||||
- [How to add cross-thread persistence (functional API)](cross-thread-persistence-functional.ipynb)
|
||||
@@ -72,7 +73,7 @@ Other methods:
|
||||
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb): Edit graph state using `graph.update_state` method. Use this if implementing a **human-in-the-loop** workflow via **static breakpoints**.
|
||||
- [How to add dynamic breakpoints with `NodeInterrupt`](human_in_the_loop/dynamic_breakpoints.ipynb): **Not recommended**: Use the [`interrupt` function](../concepts/human_in_the_loop.md) instead.
|
||||
|
||||
See the below guides for how-to implement human-in-the-loop workflows with the
|
||||
See the below guides for how-to implement human-in-the-loop workflows with the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to wait for user input (Functional API)](wait-user-input-functional.ipynb)
|
||||
@@ -129,7 +130,8 @@ These how-to guides show common patterns for tool calling with LangGraph:
|
||||
|
||||
See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for implementations of other multi-agent architectures.
|
||||
|
||||
See the below guides for how to implement multi-agent workflows with the [Functional API](../concepts/functional_api.md):
|
||||
See the below guides for how to implement multi-agent workflows with the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to build a multi-agent network (functional API)](multi-agent-network-functional.ipynb)
|
||||
- [How to add multi-turn conversation in a multi-agent application (functional API)](multi-agent-multi-turn-convo-functional.ipynb)
|
||||
@@ -147,7 +149,8 @@ See the below guides for how to implement multi-agent workflows with the [Functi
|
||||
- [How to pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
|
||||
- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](autogen-integration.ipynb)
|
||||
|
||||
See the below guide for how to integrate with other frameworks using the [Functional API](../concepts/functional_api.md):
|
||||
See the below guide for how to integrate with other frameworks using the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to integrate LangGraph (functional API) with AutoGen, CrewAI, and other frameworks](autogen-integration-functional.ipynb)
|
||||
|
||||
@@ -159,7 +162,7 @@ One of the big benefits of LangGraph is that you can easily create your own agen
|
||||
|
||||
These guides show how to use the prebuilt ReAct agent:
|
||||
|
||||
- [How to use the pre-built ReAct agent](create-react-agent.ipynb)
|
||||
- [How to use the pre-built ReAct agent](create-react-agent.md)
|
||||
- [How to add thread-level memory to a ReAct Agent](create-react-agent-memory.ipynb)
|
||||
- [How to add a custom system prompt to a ReAct agent](create-react-agent-system-prompt.ipynb)
|
||||
- [How to add human-in-the-loop processes to a ReAct agent](create-react-agent-hitl.ipynb)
|
||||
@@ -171,7 +174,8 @@ overview of its underlying implementation to help you customize for your own nee
|
||||
|
||||
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
|
||||
|
||||
See the below guide for how-to build ReAct agents with the [Functional API](../concepts/functional_api.md):
|
||||
See the below guide for how-to build ReAct agents with the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to create a ReAct agent from scratch (Functional API)](react-agent-from-scratch-functional.ipynb)
|
||||
|
||||
|
||||
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
+33
-3
@@ -1,7 +1,37 @@
|
||||
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
|
||||
# 🚀 Prebuilt Agents
|
||||
|
||||
LangGraph includes a prebuilt React agent. For more information on how to use it,
|
||||
check out our [how-to guides](https://langchain-ai.github.io/langgraph/how-tos/#prebuilt-react-agent).
|
||||
|
||||
If you’re looking for other prebuilt libraries, explore the community-built options
|
||||
below. These libraries can extend LangGraph's functionality in various ways.
|
||||
|
||||
## 📚 Available Libraries
|
||||
|
||||
[//]: # (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 | 11189 |
|
||||
| **langgraph-supervisor** | [langchain-ai/langgraph-supervisor](https://github.com/langchain-ai/langgraph-supervisor) | Build supervisor multi-agent systems with LangGraph | 1291 |
|
||||
| **breeze-agent** | [andrestorres123/breeze-agent](https://github.com/andrestorres123/breeze-agent) | A streamlined research system built inspired on STORM and built on LangGraph | 226 |
|
||||
|
||||
## ✨ Contributing Your Library
|
||||
|
||||
Have you built an awesome open-source library using LangGraph? We'd love to feature
|
||||
your project on the official LangGraph documentation pages! 🏆
|
||||
|
||||
To share your project, simply open a Pull Request adding an entry for your package in our [packages.yml](https://github.com/langchain-ai/langgraph/blob/main/docs/_scripts/third_party_page/packages.yml) file.
|
||||
|
||||
[//]: # (This file is stub. Do not edit this file directly!)
|
||||
[//]: # (1. Update the `packages.yml` file in the `docs/_scripts/third_party_page` directory.)
|
||||
[//]: # (2. From the /docs directory, run `make build-prebuilt` to generate an updated version of this file for testing locally.)
|
||||
**Guidelines**
|
||||
|
||||
- Your repo must be distributed as an installable package (e.g., PyPI for Python, npm
|
||||
for JavaScript/TypeScript, etc.) 📦
|
||||
- The repo should either use the Graph API (exposing a `StateGraph` instance) or
|
||||
the Functional API (exposing an `entrypoint`).
|
||||
- The package must include documentation (e.g., a `README.md` or docs site)
|
||||
explaining how to use it.
|
||||
|
||||
We'll review your contribution and merge it in!
|
||||
|
||||
Thanks for contributing! 🚀
|
||||
|
||||
+30
-20
@@ -56,6 +56,29 @@ plugins:
|
||||
- search:
|
||||
separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
|
||||
- autorefs
|
||||
- markdown-exec:
|
||||
ansi: required
|
||||
hooks:
|
||||
python:
|
||||
pre_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_setup
|
||||
post_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_teardown
|
||||
py:
|
||||
pre_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_setup
|
||||
post_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_teardown
|
||||
typescript:
|
||||
pre_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_setup
|
||||
post_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_teardown
|
||||
ts:
|
||||
pre_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_setup
|
||||
post_session:
|
||||
- _scripts.notebook_hooks:handle_vcr_teardown
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
@@ -100,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:
|
||||
@@ -180,7 +203,7 @@ nav:
|
||||
- how-tos/autogen-integration-functional.ipynb
|
||||
- Prebuilt ReAct Agent:
|
||||
- Prebuilt ReAct Agent: how-tos#prebuilt-react-agent
|
||||
- how-tos/create-react-agent.ipynb
|
||||
- how-tos/create-react-agent.md
|
||||
- how-tos/create-react-agent-memory.ipynb
|
||||
- how-tos/create-react-agent-system-prompt.ipynb
|
||||
- how-tos/create-react-agent-hitl.ipynb
|
||||
@@ -355,7 +378,6 @@ nav:
|
||||
- tutorials/auth/resource_auth.md
|
||||
- tutorials/auth/add_auth_server.md
|
||||
- Resources:
|
||||
# NOTE: prebuilt.md is auto-generated by `make build-prebuilt`
|
||||
- Prebuilt Agents: prebuilt.md
|
||||
- Adopters: adopters.md
|
||||
- FAQ: concepts/faq.md
|
||||
@@ -446,16 +468,6 @@ 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/
|
||||
@@ -464,9 +476,9 @@ extra:
|
||||
- icon: fontawesome/brands/twitter
|
||||
link: https://twitter.com/LangChainAI
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-WR87FQLG9F
|
||||
feedback:
|
||||
- provider: google
|
||||
- property: G-G8X6ELZYE0
|
||||
- feedback:
|
||||
title: Was this page helpful?
|
||||
ratings:
|
||||
- icon: material/emoticon-happy-outline
|
||||
@@ -497,5 +509,3 @@ validation:
|
||||
anchors: info
|
||||
# this is needed to handle headers with anchors for nav
|
||||
not_found: info
|
||||
copyright: >
|
||||
Copyright © 2025 LangChain, Inc | <a href="#__consent">Consent Preferences</a>
|
||||
|
||||
+8
-1
@@ -2,11 +2,18 @@
|
||||
"name": "docs",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"packageManager": "yarn@3.5.1",
|
||||
"scripts": {
|
||||
"build": "echo 'export OPENAI_API_KEY=\"sk-proj-1234567890\"' >> ~/.bashrc && echo 'export ANTHROPIC_API_KEY=\"sk-ant-api03-1234567890\"' >> ~/.bashrc && echo 'export PATH=$PATH:/vercel/.local/bin:$PATH' >> ~/.bashrc && source ~/.bashrc && make vercel-build-docs"
|
||||
"build": "make build-docs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^0.3.38",
|
||||
"@langchain/langgraph": "portal:./langgraphjs/libs/langgraph",
|
||||
"@langchain/langgraph-checkpoint": "portal:./langgraphjs/libs/checkpoint",
|
||||
"@langchain/langgraph-checkpoint-mongodb": "portal:./langgraphjs/libs/checkpoint-mongodb",
|
||||
"@langchain/langgraph-checkpoint-postgres": "portal:./langgraphjs/libs/checkpoint-postgres",
|
||||
"@langchain/langgraph-checkpoint-sqlite": "portal:./langgraphjs/libs/checkpoint-sqlite",
|
||||
"@langchain/langgraph-checkpoint-validation": "portal:./langgraphjs/libs/checkpoint-validation",
|
||||
"@langchain/openai": "^0.4.2",
|
||||
"msgpack-lite": "^0.1.26",
|
||||
"nock": "^14.0.1"
|
||||
|
||||
Generated
+60
-6
@@ -1016,12 +1016,12 @@ version = "0.4.6"
|
||||
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 = ["docs", "test"]
|
||||
groups = ["main", "docs", "test"]
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
markers = {docs = "python_version <= \"3.11\" or python_version >= \"3.12\"", test = "(platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\") and (python_version <= \"3.11\" or python_version >= \"3.12\")"}
|
||||
markers = {main = "sys_platform == \"win32\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", docs = "python_version <= \"3.11\" or python_version >= \"3.12\"", test = "(platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\") and (python_version <= \"3.11\" or python_version >= \"3.12\")"}
|
||||
|
||||
[[package]]
|
||||
name = "coloredlogs"
|
||||
@@ -1416,7 +1416,7 @@ version = "1.2.2"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["docs", "test"]
|
||||
groups = ["main", "docs", "test"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||
@@ -2462,6 +2462,19 @@ enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"]
|
||||
type = ["pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipykernel"
|
||||
version = "6.29.5"
|
||||
@@ -5234,7 +5247,7 @@ version = "24.2"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["docs", "test"]
|
||||
groups = ["main", "docs", "test"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
@@ -5522,6 +5535,23 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
|
||||
type = ["mypy (>=1.11.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "posthog"
|
||||
version = "3.12.1"
|
||||
@@ -6382,6 +6412,30 @@ files = [
|
||||
[package.extras]
|
||||
dev = ["build", "flake8", "mypy", "pytest", "twine"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
|
||||
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.5,<2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
@@ -7755,7 +7809,7 @@ version = "2.2.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["docs", "test"]
|
||||
groups = ["main", "docs", "test"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
|
||||
@@ -8634,4 +8688,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "06debb82135affdb2baf1fdcc028c062c236121508d787588cd0de1db2da11e4"
|
||||
content-hash = "cbcc30bb9bdead3545070eedf47fb83fe971d4914613e2188323fb3e91035e76"
|
||||
|
||||
@@ -10,6 +10,7 @@ readme = "README.md"
|
||||
python = "^3.10"
|
||||
aiohappyeyeballs = "2.4.3"
|
||||
hub = "^3.0.1"
|
||||
pytest = "^8.3.4"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
langgraph = { path = "../libs/langgraph/", develop = true }
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import re
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
from _scripts.hook_state import hook_state
|
||||
from _scripts.notebook_hooks import handle_vcr_setup
|
||||
from _scripts.setup_vcr import extract_code_blocks_for_session, get_hash_for_session
|
||||
|
||||
INITIAL_DOCUMENT_CONTENT = dedent(
|
||||
"""
|
||||
Blah blah blah
|
||||
|
||||
```python exec="on" source="above" session="1" result="ansi"
|
||||
print("FIRST_CODE_BLOCK")
|
||||
```
|
||||
|
||||
Blah blah blah!
|
||||
|
||||
```python exec="on" source="above" session="1" result="ansi"
|
||||
print("SECOND_CODE_BLOCK")
|
||||
```
|
||||
|
||||
more blah blah blah
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"replace_string",
|
||||
[
|
||||
"FIRST_CODE_BLOCK",
|
||||
"SECOND_CODE_BLOCK",
|
||||
],
|
||||
)
|
||||
def test_changing_block_in_session_invalidates_hash(replace_string: str):
|
||||
hook_state['document_filename'] = 'test.md'
|
||||
hook_state['document_content'] = INITIAL_DOCUMENT_CONTENT
|
||||
|
||||
code = "print('Hello, world!')"
|
||||
|
||||
result1 = handle_vcr_setup(
|
||||
formatter=lambda **kwargs: None,
|
||||
language="python",
|
||||
session="1",
|
||||
id="test",
|
||||
code=code,
|
||||
md=None,
|
||||
extra={},
|
||||
)
|
||||
|
||||
cassette_init_expr = re.compile(r"^_cassette = HashedCassette\('[^']+', '(?P<hash>[^']+)'\)$")
|
||||
assert result1['transform_source']
|
||||
execute_source, display_source = result1['transform_source'](code)
|
||||
assert display_source == code
|
||||
cassette_init_line = [line for line in execute_source.splitlines() if line.startswith("_cassette = HashedCassette(")][0]
|
||||
assert cassette_init_line
|
||||
match = cassette_init_expr.match(cassette_init_line)
|
||||
assert match
|
||||
hash_ = str(match.group('hash'))
|
||||
|
||||
# change the content of the second block of code
|
||||
hook_state['document_content'] = INITIAL_DOCUMENT_CONTENT.replace(replace_string, "world")
|
||||
assert hook_state['document_content'] != INITIAL_DOCUMENT_CONTENT
|
||||
|
||||
result2 = handle_vcr_setup(
|
||||
formatter=lambda **kwargs: None,
|
||||
language="python",
|
||||
session="1",
|
||||
id="test",
|
||||
code=code,
|
||||
md=None,
|
||||
extra={},
|
||||
)
|
||||
|
||||
assert result2['transform_source']
|
||||
execute_source, display_source = result2['transform_source'](code)
|
||||
assert display_source == code
|
||||
cassette_init_line = [line for line in execute_source.splitlines() if line.startswith("_cassette = HashedCassette(")][0]
|
||||
assert cassette_init_line
|
||||
match = cassette_init_expr.match(cassette_init_line)
|
||||
assert match
|
||||
|
||||
# this is the important part
|
||||
assert str(match.group('hash')) != hash_
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"replace_string",
|
||||
[
|
||||
"FIRST_CODE_BLOCK",
|
||||
"SECOND_CODE_BLOCK",
|
||||
],
|
||||
)
|
||||
def test_get_hash_for_session(replace_string: str):
|
||||
hash_ = get_hash_for_session(
|
||||
language="python",
|
||||
session="1",
|
||||
content=INITIAL_DOCUMENT_CONTENT,
|
||||
)
|
||||
|
||||
content = INITIAL_DOCUMENT_CONTENT.replace(replace_string, "world")
|
||||
assert content != INITIAL_DOCUMENT_CONTENT
|
||||
assert get_hash_for_session(
|
||||
language="python",
|
||||
session="1",
|
||||
content=INITIAL_DOCUMENT_CONTENT.replace(replace_string, "world"),
|
||||
) != hash_
|
||||
|
||||
def test_get_code_blocks_for_session():
|
||||
code_blocks = extract_code_blocks_for_session(
|
||||
language="python",
|
||||
session="1",
|
||||
content=INITIAL_DOCUMENT_CONTENT,
|
||||
)
|
||||
assert code_blocks
|
||||
assert len(code_blocks) == 2
|
||||
assert code_blocks[0] == 'print("FIRST_CODE_BLOCK")'
|
||||
assert code_blocks[1] == 'print("SECOND_CODE_BLOCK")'
|
||||
@@ -111,7 +111,7 @@ print("Hello, World!")
|
||||
|
||||
|
||||
END_TO_END_INPUT_HIGHLIGHT_1_EXPECT = """\
|
||||
```python exec="on" source="below" hl_lines="2" path="dummy.md"
|
||||
```python exec="on" source="below" hl_lines="2"
|
||||
print("Hello, World!")
|
||||
print("Hello, World!")
|
||||
```
|
||||
|
||||
@@ -1,10 +1,81 @@
|
||||
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."""
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"buildCommand": "yarn build",
|
||||
"outputDirectory": "site"
|
||||
"buildCommand": "echo 'export OPENAI_API_KEY=\"sk-proj-1234567890\"' >> ~/.bashrc && echo 'export ANTHROPIC_API_KEY=\"sk-ant-api03-1234567890\"' >> ~/.bashrc && echo 'export PATH=$PATH:/vercel/.local/bin:$PATH' >> ~/.bashrc && source ~/.bashrc && make vercel-build-docs",
|
||||
"outputDirectory": "site",
|
||||
"installCommand": "echo done"
|
||||
}
|
||||
|
||||
+4823
-408
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@ 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
|
||||
@@ -318,7 +317,17 @@ class PostgresSaver(BasePostgresSaver):
|
||||
checkpoint["id"],
|
||||
checkpoint_id,
|
||||
Jsonb(self._dump_checkpoint(copy)),
|
||||
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
|
||||
self._dump_metadata(
|
||||
{
|
||||
**{
|
||||
k: v
|
||||
for k, v in config["configurable"].items()
|
||||
if not k.startswith("__")
|
||||
},
|
||||
**config.get("metadata", {}),
|
||||
**metadata,
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
|
||||
@@ -16,7 +16,6 @@ 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
|
||||
@@ -276,7 +275,17 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
checkpoint["id"],
|
||||
checkpoint_id,
|
||||
Jsonb(self._dump_checkpoint(copy)),
|
||||
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
|
||||
self._dump_metadata(
|
||||
{
|
||||
**{
|
||||
k: v
|
||||
for k, v in config["configurable"].items()
|
||||
if not k.startswith("__")
|
||||
},
|
||||
**config.get("metadata", {}),
|
||||
**metadata,
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
|
||||
@@ -24,7 +24,6 @@ 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
|
||||
@@ -424,7 +423,17 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
Jsonb(self._dump_checkpoint(copy)),
|
||||
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
|
||||
self._dump_metadata(
|
||||
{
|
||||
**{
|
||||
k: v
|
||||
for k, v in config["configurable"].items()
|
||||
if not k.startswith("__")
|
||||
},
|
||||
**config.get("metadata", {}),
|
||||
**metadata,
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
@@ -743,7 +752,17 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
Jsonb(self._dump_checkpoint(copy)),
|
||||
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
|
||||
self._dump_metadata(
|
||||
{
|
||||
**{
|
||||
k: v
|
||||
for k, v in config["configurable"].items()
|
||||
if not k.startswith("__")
|
||||
},
|
||||
**config.get("metadata", {}),
|
||||
**metadata,
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
|
||||
Generated
+22
-11
@@ -187,12 +187,22 @@ 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"
|
||||
@@ -348,7 +358,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.15"
|
||||
version = "2.0.10"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -952,20 +962,21 @@ pytest = ">=6.2.5"
|
||||
dev = ["pre-commit", "pytest-asyncio", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-watcher"
|
||||
version = "0.4.3"
|
||||
description = "Automatically rerun your tests on file modifications"
|
||||
name = "pytest-watch"
|
||||
version = "4.2.0"
|
||||
description = "Local continuous test runner with pytest and watchdog."
|
||||
optional = false
|
||||
python-versions = "<4.0.0,>=3.7.0"
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest_watcher-0.4.3-py3-none-any.whl", hash = "sha256:d59b1e1396f33a65ea4949b713d6884637755d641646960056a90b267c3460f9"},
|
||||
{file = "pytest_watcher-0.4.3.tar.gz", hash = "sha256:0cb0e4661648c8c0ff2b2d25efa5a8e421784b9e4c60fcecbf9b7c30b2d731b3"},
|
||||
{file = "pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
|
||||
watchdog = ">=2.0.0"
|
||||
colorama = ">=0.3.3"
|
||||
docopt = ">=0.4.0"
|
||||
pytest = ">=2.6.4"
|
||||
watchdog = ">=0.6.0"
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
@@ -1255,4 +1266,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "369bfffecb9489835b43b8255932e043176a11d2f639aad2d055ffd89263ca1e"
|
||||
content-hash = "61326e4e81a4e8854763a119f39d4f5d0a54cee868b4dbc91b95ce7d2cebba5b"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.15"
|
||||
version = "2.0.14"
|
||||
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.15"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
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,9 +61,3 @@ 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"]
|
||||
|
||||
@@ -11,7 +11,6 @@ from psycopg.rows import dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
EXCLUDED_METADATA_KEYS,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
create_checkpoint,
|
||||
@@ -24,10 +23,6 @@ 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."""
|
||||
@@ -228,6 +223,7 @@ 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",
|
||||
}
|
||||
|
||||
@@ -255,14 +251,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 == {
|
||||
**_exclude_keys(configs[0]["configurable"]),
|
||||
**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 == {
|
||||
**_exclude_keys(configs[1]["configurable"]),
|
||||
**configs[1]["configurable"],
|
||||
**metadata[1],
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from psycopg.rows import dict_row
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
EXCLUDED_METADATA_KEYS,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
create_checkpoint,
|
||||
@@ -22,10 +21,6 @@ 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."""
|
||||
@@ -210,6 +205,7 @@ 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",
|
||||
}
|
||||
|
||||
@@ -237,14 +233,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 == {
|
||||
**_exclude_keys(configs[0]["configurable"]),
|
||||
**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 == {
|
||||
**_exclude_keys(configs[1]["configurable"]),
|
||||
**configs[1]["configurable"],
|
||||
**metadata[1],
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ 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
|
||||
@@ -399,7 +398,15 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
||||
serialized_metadata = self.jsonplus_serde.dumps(
|
||||
get_checkpoint_metadata(config, metadata)
|
||||
{
|
||||
**{
|
||||
k: v
|
||||
for k, v in config["configurable"].items()
|
||||
if not k.startswith("__")
|
||||
},
|
||||
**config.get("metadata", {}),
|
||||
**metadata,
|
||||
}
|
||||
)
|
||||
with self.cursor() as cur:
|
||||
cur.execute(
|
||||
|
||||
@@ -16,7 +16,6 @@ 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
|
||||
@@ -465,7 +464,15 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
||||
serialized_metadata = self.jsonplus_serde.dumps(
|
||||
get_checkpoint_metadata(config, metadata)
|
||||
{
|
||||
**{
|
||||
k: v
|
||||
for k, v in config["configurable"].items()
|
||||
if not k.startswith("__")
|
||||
},
|
||||
**config.get("metadata", {}),
|
||||
**metadata,
|
||||
}
|
||||
)
|
||||
async with (
|
||||
self.lock,
|
||||
|
||||
Generated
+2
-2
@@ -350,7 +350,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.15"
|
||||
version = "2.0.10"
|
||||
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 = "e6d3ca9bce723c05f4c5ae9dc4bee872f7581b7763680b34112f1d280f5a9b0a"
|
||||
content-hash = "03c697eae6f550f3c7e29f1d61f4c409dabe04ae8d43281728e549174d2fc670"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "2.0.5"
|
||||
version = "2.0.4"
|
||||
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.15"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
aiosqlite = "^0.20.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -72,6 +72,7 @@ class TestAsyncSqliteSaver:
|
||||
assert checkpoint.metadata == {
|
||||
**self.metadata_2,
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "",
|
||||
"run_id": "my_run_id",
|
||||
}
|
||||
|
||||
@@ -93,15 +94,14 @@ 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 == {
|
||||
"thread_id": "thread-1",
|
||||
"thread_ts": "1",
|
||||
**self.config_1["configurable"],
|
||||
**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 == {
|
||||
"thread_id": "thread-2",
|
||||
**self.config_2["configurable"],
|
||||
**self.metadata_2,
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ class TestSqliteSaver:
|
||||
assert checkpoint.metadata == {
|
||||
**self.metadata_2,
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "",
|
||||
"run_id": "my_run_id",
|
||||
}
|
||||
|
||||
@@ -96,15 +97,14 @@ class TestSqliteSaver:
|
||||
search_results_1 = list(saver.list(None, filter=query_1))
|
||||
assert len(search_results_1) == 1
|
||||
assert search_results_1[0].metadata == {
|
||||
"thread_id": "thread-1",
|
||||
"thread_ts": "1",
|
||||
**self.config_1["configurable"],
|
||||
**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 == {
|
||||
"thread_id": "thread-2",
|
||||
**self.config_2["configurable"],
|
||||
**self.metadata_2,
|
||||
}
|
||||
|
||||
|
||||
@@ -446,23 +446,6 @@ 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.
|
||||
@@ -471,9 +454,3 @@ 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,7 +20,6 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointTuple,
|
||||
SerializerProtocol,
|
||||
get_checkpoint_id,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import TASKS, ChannelProtocol
|
||||
|
||||
@@ -357,7 +356,17 @@ class InMemorySaver(
|
||||
{
|
||||
checkpoint["id"]: (
|
||||
self.serde.dumps_typed(c),
|
||||
self.serde.dumps_typed(get_checkpoint_metadata(config, metadata)),
|
||||
self.serde.dumps_typed(
|
||||
{
|
||||
**{
|
||||
k: v
|
||||
for k, v in config["configurable"].items()
|
||||
if not k.startswith("__")
|
||||
},
|
||||
**config.get("metadata", {}),
|
||||
**metadata,
|
||||
}
|
||||
),
|
||||
config["configurable"].get("checkpoint_id"), # parent
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import json
|
||||
import pathlib
|
||||
import re
|
||||
from collections import deque
|
||||
from collections.abc import Sequence
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
from enum import Enum
|
||||
from inspect import isclass
|
||||
@@ -17,7 +16,7 @@ from ipaddress import (
|
||||
IPv6Interface,
|
||||
IPv6Network,
|
||||
)
|
||||
from typing import Any, Callable, Optional, Union, cast
|
||||
from typing import Any, Callable, Optional, Sequence, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
import msgpack # type: ignore[import-untyped]
|
||||
@@ -503,5 +502,15 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return
|
||||
|
||||
|
||||
ENC_POOL: deque[msgpack.Packer] = deque(maxlen=32)
|
||||
|
||||
|
||||
def _msgpack_enc(data: Any) -> bytes:
|
||||
return msgpack.packb(data, default=_msgpack_default)
|
||||
try:
|
||||
enc = ENC_POOL.popleft()
|
||||
except IndexError:
|
||||
enc = msgpack.Packer(default=_msgpack_default)
|
||||
try:
|
||||
return enc.pack(data)
|
||||
finally:
|
||||
ENC_POOL.append(enc)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import asyncio
|
||||
import functools
|
||||
import weakref
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Callable, Literal, Optional, TypeVar, Union
|
||||
from typing import Any, Callable, Iterable, Literal, Optional, TypeVar, Union
|
||||
|
||||
from langgraph.store.base import (
|
||||
BaseStore,
|
||||
@@ -55,23 +54,19 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._loop = asyncio.get_running_loop()
|
||||
self._aqueue: asyncio.Queue[tuple[asyncio.Future, Op]] = asyncio.Queue()
|
||||
self._aqueue: dict[asyncio.Future, Op] = {}
|
||||
self._task = self._loop.create_task(_run(self._aqueue, weakref.ref(self)))
|
||||
|
||||
def __del__(self) -> None:
|
||||
try:
|
||||
self._task.cancel()
|
||||
except RuntimeError:
|
||||
pass
|
||||
self._task.cancel()
|
||||
|
||||
async def aget(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
) -> Optional[Item]:
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait((fut, GetOp(namespace, key)))
|
||||
self._aqueue[fut] = GetOp(namespace, key)
|
||||
return await fut
|
||||
|
||||
async def asearch(
|
||||
@@ -84,11 +79,8 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
) -> list[SearchItem]:
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait(
|
||||
(fut, SearchOp(namespace_prefix, filter, limit, offset, query))
|
||||
)
|
||||
self._aqueue[fut] = SearchOp(namespace_prefix, filter, limit, offset, query)
|
||||
return await fut
|
||||
|
||||
async def aput(
|
||||
@@ -98,10 +90,9 @@ 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.put_nowait((fut, PutOp(namespace, key, value, index)))
|
||||
self._aqueue[fut] = PutOp(namespace, key, value, index)
|
||||
return await fut
|
||||
|
||||
async def adelete(
|
||||
@@ -109,9 +100,8 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
) -> None:
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait((fut, PutOp(namespace, key, None)))
|
||||
self._aqueue[fut] = PutOp(namespace, key, None)
|
||||
return await fut
|
||||
|
||||
async def alist_namespaces(
|
||||
@@ -123,7 +113,6 @@ 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:
|
||||
@@ -137,7 +126,7 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
self._aqueue.put_nowait((fut, op))
|
||||
self._aqueue[fut] = op
|
||||
return await fut
|
||||
|
||||
@_check_loop
|
||||
@@ -261,38 +250,34 @@ def _dedupe_ops(values: list[Op]) -> tuple[Optional[list[int]], list[Op]]:
|
||||
|
||||
|
||||
async def _run(
|
||||
aqueue: asyncio.Queue[tuple[asyncio.Future, Op]],
|
||||
aqueue: dict[asyncio.Future, Op],
|
||||
store: weakref.ReferenceType[BaseStore],
|
||||
) -> None:
|
||||
while item := await aqueue.get():
|
||||
# check if store is still alive
|
||||
while True:
|
||||
await asyncio.sleep(0)
|
||||
if not aqueue:
|
||||
continue
|
||||
if s := store():
|
||||
# get the operations to run
|
||||
taken = aqueue.copy()
|
||||
# action each operation
|
||||
try:
|
||||
# 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]
|
||||
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]
|
||||
|
||||
# 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
|
||||
# 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]
|
||||
else:
|
||||
break
|
||||
# remove strong ref to store
|
||||
del s
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.16"
|
||||
version = "2.0.13"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -60,7 +60,7 @@ class TestMemorySaver:
|
||||
self.metadata_3: CheckpointMetadata = {}
|
||||
|
||||
def test_combined_metadata(self) -> None:
|
||||
config: RunnableConfig = {
|
||||
config = {
|
||||
"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,15 +96,14 @@ 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 == {
|
||||
"thread_id": "thread-1",
|
||||
"thread_ts": "1",
|
||||
**self.config_1["configurable"],
|
||||
**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 == {
|
||||
"thread_id": "thread-2",
|
||||
**self.config_2["configurable"],
|
||||
**self.metadata_2,
|
||||
}
|
||||
|
||||
@@ -147,8 +146,7 @@ class TestMemorySaver:
|
||||
]
|
||||
assert len(search_results_1) == 1
|
||||
assert search_results_1[0].metadata == {
|
||||
"thread_id": "thread-1",
|
||||
"thread_ts": "1",
|
||||
**self.config_1["configurable"],
|
||||
**self.metadata_1,
|
||||
}
|
||||
|
||||
@@ -157,7 +155,7 @@ class TestMemorySaver:
|
||||
]
|
||||
assert len(search_results_2) == 1
|
||||
assert search_results_2[0].metadata == {
|
||||
"thread_id": "thread-2",
|
||||
**self.config_2["configurable"],
|
||||
**self.metadata_2,
|
||||
}
|
||||
|
||||
|
||||
@@ -303,15 +303,7 @@ def _build(
|
||||
tag,
|
||||
]
|
||||
# apply config
|
||||
stdin, additional_contexts = langgraph_cli.config.config_to_docker(
|
||||
config, config_json, base_image
|
||||
)
|
||||
# add additional_contexts
|
||||
if additional_contexts:
|
||||
additional_contexts_str = ",".join(
|
||||
f"{k}={v}" for k, v in additional_contexts.items()
|
||||
)
|
||||
args.extend(["--build-context", additional_contexts_str])
|
||||
stdin = langgraph_cli.config.config_to_docker(config, config_json, base_image)
|
||||
# run docker build
|
||||
runner.run(
|
||||
subp_exec(
|
||||
@@ -447,28 +439,20 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -
|
||||
secho("✅ Configuration validated!", fg="green")
|
||||
|
||||
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
|
||||
dockerfile, additional_contexts = langgraph_cli.config.config_to_docker(
|
||||
config,
|
||||
config_json,
|
||||
(
|
||||
"langchain/langgraphjs-api"
|
||||
if config_json.get("node_version")
|
||||
else "langchain/langgraph-api"
|
||||
),
|
||||
)
|
||||
with open(str(save_path), "w", encoding="utf-8") as f:
|
||||
f.write(dockerfile)
|
||||
f.write(
|
||||
langgraph_cli.config.config_to_docker(
|
||||
config,
|
||||
config_json,
|
||||
(
|
||||
"langchain/langgraphjs-api"
|
||||
if config_json.get("node_version")
|
||||
else "langchain/langgraph-api"
|
||||
),
|
||||
)
|
||||
)
|
||||
secho("✅ Created: Dockerfile", fg="green")
|
||||
|
||||
if additional_contexts:
|
||||
additional_contexts_str = ",".join(
|
||||
f"{k}={v}" for k, v in additional_contexts.items()
|
||||
)
|
||||
secho(
|
||||
f"""📝 Run docker build with these additional build contexts `--build-context {additional_contexts_str}`""",
|
||||
fg="yellow",
|
||||
)
|
||||
|
||||
if add_docker_compose:
|
||||
# Add docker compose and related files
|
||||
# Add .dockerignore file in the same directory as the Dockerfile
|
||||
@@ -591,7 +575,7 @@ def dev(
|
||||
):
|
||||
"""CLI entrypoint for running the LangGraph API server."""
|
||||
try:
|
||||
from langgraph_api.cli import run_server # type: ignore
|
||||
from langgraph_api.cli import run_server
|
||||
except ImportError:
|
||||
py_version_msg = ""
|
||||
if sys.version_info < (3, 11):
|
||||
@@ -678,7 +662,6 @@ def prepare_args_and_stdin(
|
||||
debugger_base_url: Optional[str] = None,
|
||||
postgres_uri: Optional[str] = None,
|
||||
) -> Tuple[List[str], str]:
|
||||
assert config_path.exists(), f"Config file not found: {config_path}"
|
||||
# prepare args
|
||||
stdin = langgraph_cli.docker.compose(
|
||||
capabilities,
|
||||
|
||||
@@ -2,7 +2,6 @@ import json
|
||||
import os
|
||||
import pathlib
|
||||
import textwrap
|
||||
from collections import Counter
|
||||
from typing import NamedTuple, Optional, TypedDict, Union
|
||||
|
||||
import click
|
||||
@@ -295,10 +294,10 @@ class LocalDeps(NamedTuple):
|
||||
tuples. Each entry points to a local `requirements.txt` file and where
|
||||
it should be placed inside the Docker container before running `pip install`.
|
||||
|
||||
real_pkgs: A dictionary mapping a local directory path (host side) to a
|
||||
tuple of (dependency_string, container_package_path). These directories
|
||||
contain the necessary files (e.g., `pyproject.toml` or `setup.py`) to be
|
||||
installed as a standard Python package with pip.
|
||||
real_pkgs: A dictionary mapping a local directory path (host side) to the
|
||||
same dependency string from the config. These directories contain the
|
||||
necessary files (e.g., `pyproject.toml` or `setup.py`) to be installed
|
||||
as a standard Python package with pip.
|
||||
|
||||
faux_pkgs: A dictionary mapping a local directory path (host side) to a
|
||||
tuple of (dependency_string, container_package_path). For these
|
||||
@@ -311,23 +310,16 @@ class LocalDeps(NamedTuple):
|
||||
directory. If the local dependency `"."` is present in the config, this
|
||||
field captures the path where that dependency will appear in the
|
||||
container (e.g., `/deps/<name>` or similar). Otherwise, it may be `None`.
|
||||
|
||||
additional_contexts: A list of paths to directories that contain local
|
||||
dependencies in parent directories. These directories are added to the
|
||||
Docker build context to ensure that the Dockerfile can access them.
|
||||
"""
|
||||
|
||||
pip_reqs: list[tuple[pathlib.Path, str]]
|
||||
real_pkgs: dict[pathlib.Path, tuple[str, str]]
|
||||
pip_reqs: list[tuple[str, str]]
|
||||
real_pkgs: dict[pathlib.Path, str]
|
||||
faux_pkgs: dict[pathlib.Path, tuple[str, str]]
|
||||
# if . is in dependencies, use it as working_dir
|
||||
working_dir: Optional[str] = None
|
||||
# if there are local dependencies in parent directories, use additional_contexts
|
||||
additional_contexts: list[pathlib.Path] = None
|
||||
|
||||
|
||||
def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps:
|
||||
config_path = config_path.resolve()
|
||||
# ensure reserved package names are not used
|
||||
reserved = {
|
||||
"src",
|
||||
@@ -344,7 +336,6 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
"httpx",
|
||||
"langsmith",
|
||||
}
|
||||
counter = Counter()
|
||||
|
||||
def check_reserved(name: str, ref: str):
|
||||
if name in reserved:
|
||||
@@ -357,8 +348,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
pip_reqs = []
|
||||
real_pkgs = {}
|
||||
faux_pkgs = {}
|
||||
working_dir: Optional[str] = None
|
||||
additional_contexts: list[pathlib.Path] = []
|
||||
working_dir = None
|
||||
|
||||
for local_dep in config["dependencies"]:
|
||||
if not local_dep.startswith("."):
|
||||
@@ -367,7 +357,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
|
||||
# Verify that the local dependency can be resolved
|
||||
# (e.g., this would raise an informative error if a user mistyped a path).
|
||||
resolved = (config_path.parent / local_dep).resolve()
|
||||
resolved = config_path.parent / local_dep
|
||||
|
||||
# validate local dependency
|
||||
if not resolved.exists():
|
||||
@@ -376,28 +366,25 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
raise NotADirectoryError(
|
||||
f"Local dependency must be a directory: {resolved}"
|
||||
)
|
||||
elif resolved == config_path.parent:
|
||||
pass
|
||||
elif config_path.parent not in resolved.parents:
|
||||
additional_contexts.append(resolved)
|
||||
elif not resolved.is_relative_to(config_path.parent):
|
||||
raise ValueError(
|
||||
f"Local dependency '{resolved}' must be a subdirectory of '{config_path.parent}'"
|
||||
)
|
||||
|
||||
# Check for pyproject.toml or setup.py
|
||||
# If found, treat as a real package, if not treat as a faux package.
|
||||
# For faux packages, we'll also check for presence of requirements.txt.
|
||||
files = os.listdir(resolved)
|
||||
if "pyproject.toml" in files or "setup.py" in files:
|
||||
if "pyproject.toml" in files:
|
||||
# real package
|
||||
|
||||
# assign a unique folder name
|
||||
container_name = resolved.name
|
||||
if counter[container_name] > 0:
|
||||
container_name += f"_{counter[container_name]}"
|
||||
counter[container_name] += 1
|
||||
# add to deps
|
||||
real_pkgs[resolved] = (local_dep, container_name)
|
||||
# set working_dir
|
||||
real_pkgs[resolved] = local_dep
|
||||
if local_dep == ".":
|
||||
working_dir = f"/deps/{container_name}"
|
||||
working_dir = f"/deps/{resolved.name}"
|
||||
elif "setup.py" in files:
|
||||
# real package
|
||||
real_pkgs[resolved] = local_dep
|
||||
if local_dep == ".":
|
||||
working_dir = f"/deps/{resolved.name}"
|
||||
else:
|
||||
# We could not find a pyproject.toml or setup.py, so treat as a faux package
|
||||
if any(file == "__init__.py" for file in files):
|
||||
@@ -436,12 +423,12 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
rfile = resolved / "requirements.txt"
|
||||
pip_reqs.append(
|
||||
(
|
||||
rfile,
|
||||
rfile.relative_to(config_path.parent).as_posix(),
|
||||
f"{container_path}/requirements.txt",
|
||||
)
|
||||
)
|
||||
|
||||
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir, additional_contexts)
|
||||
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir)
|
||||
|
||||
|
||||
def _update_graph_paths(
|
||||
@@ -569,7 +556,7 @@ def _update_auth_path(
|
||||
|
||||
def python_config_to_docker(
|
||||
config_path: pathlib.Path, config: Config, base_image: str
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
) -> str:
|
||||
"""Generate a Dockerfile from the configuration."""
|
||||
# configure pip
|
||||
pip_install = (
|
||||
@@ -594,15 +581,9 @@ def python_config_to_docker(
|
||||
pip_pkgs_str = f"RUN {pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
|
||||
if local_deps.pip_reqs:
|
||||
pip_reqs_str = os.linesep.join(
|
||||
f"COPY --from=__outer_{reqpath.name} requirements.txt {destpath}"
|
||||
if reqpath.parent in local_deps.additional_contexts
|
||||
else f"ADD {reqpath.relative_to(config_path.parent)} {destpath}"
|
||||
for reqpath, destpath in local_deps.pip_reqs
|
||||
f"ADD {reqpath} {destpath}" for reqpath, destpath in local_deps.pip_reqs
|
||||
)
|
||||
pip_reqs_str += f'{os.linesep}RUN {pip_install} {" ".join("-r " + r for _,r in local_deps.pip_reqs)}'
|
||||
pip_reqs_str = f"""# -- Installing local requirements --
|
||||
{pip_reqs_str}
|
||||
# -- End of local requirements install --"""
|
||||
|
||||
else:
|
||||
pip_reqs_str = ""
|
||||
@@ -610,14 +591,7 @@ def python_config_to_docker(
|
||||
# https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data
|
||||
# https://til.simonwillison.net/python/pyproject
|
||||
faux_pkgs_str = f"{os.linesep}{os.linesep}".join(
|
||||
(
|
||||
f"""# -- Adding non-package dependency {fullpath.name} --
|
||||
COPY --from=__outer_{fullpath.name} . {destpath}"""
|
||||
if fullpath in local_deps.additional_contexts
|
||||
else f"""# -- Adding non-package dependency {fullpath.name} --
|
||||
ADD {relpath} {destpath}"""
|
||||
)
|
||||
+ f"""
|
||||
f"""ADD {relpath} {destpath}
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
'name = "{fullpath.name}"' \\
|
||||
@@ -625,20 +599,12 @@ RUN set -ex && \\
|
||||
'[tool.setuptools.package-data]' \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_{fullpath.name}/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency {fullpath.name} --"""
|
||||
done"""
|
||||
for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items()
|
||||
)
|
||||
|
||||
local_pkgs_str = os.linesep.join(
|
||||
f"""# -- Adding local package {relpath} --
|
||||
COPY --from={name} . /deps/{name}
|
||||
# -- End of local package {relpath} --"""
|
||||
if fullpath in local_deps.additional_contexts
|
||||
else f"""# -- Adding local package {relpath} --
|
||||
ADD {relpath} /deps/{name}
|
||||
# -- End of local package {relpath} --"""
|
||||
for fullpath, (relpath, name) in local_deps.real_pkgs.items()
|
||||
f"ADD {relpath} /deps/{fullpath.name}"
|
||||
for fullpath, relpath in local_deps.real_pkgs.items()
|
||||
)
|
||||
|
||||
installs = f"{os.linesep}{os.linesep}".join(
|
||||
@@ -672,30 +638,15 @@ ADD {relpath} /deps/{name}
|
||||
"",
|
||||
installs,
|
||||
"",
|
||||
"# -- Installing all local dependencies --",
|
||||
f"RUN {pip_install} -e /deps/*",
|
||||
"# -- End of local dependencies install --",
|
||||
os.linesep.join(env_vars),
|
||||
"",
|
||||
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
|
||||
]
|
||||
|
||||
additional_contexts: dict[str, str] = {}
|
||||
for p in local_deps.additional_contexts:
|
||||
if p in local_deps.real_pkgs:
|
||||
name = local_deps.real_pkgs[p][1]
|
||||
elif p in local_deps.faux_pkgs:
|
||||
name = f"__outer_{p.name}"
|
||||
else:
|
||||
raise RuntimeError(f"Unknown additional context: {p}")
|
||||
additional_contexts[name] = str(p)
|
||||
|
||||
return os.linesep.join(docker_file_contents), additional_contexts
|
||||
return os.linesep.join(docker_file_contents)
|
||||
|
||||
|
||||
def node_config_to_docker(
|
||||
config_path: pathlib.Path, config: Config, base_image: str
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
|
||||
faux_path = f"/deps/{config_path.parent.name}"
|
||||
|
||||
def test_file(file_name):
|
||||
@@ -735,8 +686,7 @@ ENV LANGGRAPH_STORE='{json.dumps(store_config)}'
|
||||
ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'
|
||||
"""
|
||||
|
||||
return (
|
||||
f"""FROM {base_image}:{config['node_version']}
|
||||
return f"""FROM {base_image}:{config['node_version']}
|
||||
|
||||
{os.linesep.join(config["dockerfile_lines"])}
|
||||
|
||||
@@ -748,14 +698,10 @@ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
||||
|
||||
WORKDIR {faux_path}
|
||||
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts""",
|
||||
{},
|
||||
)
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
|
||||
|
||||
|
||||
def config_to_docker(
|
||||
config_path: pathlib.Path, config: Config, base_image: str
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
|
||||
if config.get("node_version"):
|
||||
return node_config_to_docker(config_path, config, base_image)
|
||||
|
||||
@@ -791,24 +737,13 @@ def config_to_compose(
|
||||
else:
|
||||
watch_str = ""
|
||||
|
||||
dockerfile, additional_contexts = config_to_docker(config_path, config, base_image)
|
||||
|
||||
additional_contexts_str = "\n".join(
|
||||
f" - {name}: {path}"
|
||||
for name, path in additional_contexts.items()
|
||||
)
|
||||
if additional_contexts_str:
|
||||
additional_contexts_str = f"""
|
||||
additional_contexts:
|
||||
{additional_contexts_str}"""
|
||||
|
||||
return f"""
|
||||
{textwrap.indent(env_vars_str, " ")}
|
||||
{env_file_str}
|
||||
pull_policy: build
|
||||
build:
|
||||
context: .{additional_contexts_str}
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
{textwrap.indent(dockerfile, " ")}
|
||||
{textwrap.indent(config_to_docker(config_path, config, base_image), " ")}
|
||||
{watch_str}
|
||||
"""
|
||||
|
||||
@@ -49,9 +49,7 @@ def check_capabilities(runner) -> DockerCapabilities:
|
||||
raise click.UsageError("Docker not installed") from None
|
||||
|
||||
try:
|
||||
stdout, _ = runner.run(
|
||||
subp_exec("docker", "info", "-f", "{{json .}}", collect=True)
|
||||
)
|
||||
stdout, _ = runner.run(subp_exec("docker", "info", "-f", "json", collect=True))
|
||||
info = json.loads(stdout)
|
||||
except (click.exceptions.Exit, json.JSONDecodeError):
|
||||
raise click.UsageError("Docker not installed or not running") from None
|
||||
|
||||
@@ -40,9 +40,9 @@ def temporary_config_folder(config_content: dict):
|
||||
|
||||
def test_prepare_args_and_stdin() -> None:
|
||||
# this basically serves as an end-to-end test for using config and docker helpers
|
||||
config_path = pathlib.Path(__file__).parent / "langgraph.json"
|
||||
config_path = pathlib.Path("./langgraph.json")
|
||||
config = validate_config(
|
||||
Config(dependencies=[".", "../../.."], graphs={"agent": "agent.py:graph"})
|
||||
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
|
||||
)
|
||||
port = 8000
|
||||
debugger_port = 8001
|
||||
@@ -61,7 +61,7 @@ def test_prepare_args_and_stdin() -> None:
|
||||
|
||||
expected_args = [
|
||||
"--project-directory",
|
||||
str(pathlib.Path(__file__).parent.absolute()),
|
||||
".",
|
||||
"-f",
|
||||
"custom-docker-compose.yml",
|
||||
"-f",
|
||||
@@ -129,29 +129,18 @@ services:
|
||||
pull_policy: build
|
||||
build:
|
||||
context: .
|
||||
additional_contexts:
|
||||
- cli_1: {str(pathlib.Path(__file__).parent.parent.parent.parent.absolute())}
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding local package . --
|
||||
ADD . /deps/cli
|
||||
# -- End of local package . --
|
||||
# -- Adding local package ../../.. --
|
||||
COPY --from=cli_1 . /deps/cli_1
|
||||
# -- End of local package ../../.. --
|
||||
# -- Installing all local dependencies --
|
||||
ADD . /deps/
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
||||
WORKDIR /deps/cli
|
||||
WORKDIR /deps/
|
||||
|
||||
develop:
|
||||
watch:
|
||||
- path: langgraph.json
|
||||
action: rebuild
|
||||
- path: .
|
||||
action: rebuild
|
||||
- path: ../../..
|
||||
action: rebuild\
|
||||
"""
|
||||
assert actual_args == expected_args
|
||||
|
||||
@@ -177,20 +177,13 @@ def test_validate_config_file():
|
||||
# config_to_docker
|
||||
def test_config_to_docker_simple():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
actual_docker_stdin = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{"dependencies": [".", "../../examples/graphs_reqs_a"], "graphs": graphs}
|
||||
),
|
||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Installing local requirements --
|
||||
COPY --from=__outer_requirements.txt requirements.txt /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -r /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||
# -- End of local requirements install --
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -200,79 +193,16 @@ RUN set -ex && \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Adding non-package dependency graphs_reqs_a --
|
||||
COPY --from=__outer_graphs_reqs_a . /deps/__outer_graphs_reqs_a/graphs_reqs_a
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
'name = "graphs_reqs_a"' \\
|
||||
'version = "0.1"' \\
|
||||
'[tool.setuptools.package-data]' \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_graphs_reqs_a/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency graphs_reqs_a --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||
"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
|
||||
assert additional_contexts == {
|
||||
"__outer_graphs_reqs_a": str(
|
||||
(pathlib.Path(__file__).parent / "../../examples/graphs_reqs_a").resolve()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def test_config_to_docker_outside_path():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": [".", ".."], "graphs": graphs}),
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
'name = "unit_tests"' \\
|
||||
'version = "0.1"' \\
|
||||
'[tool.setuptools.package-data]' \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Adding non-package dependency tests --
|
||||
COPY --from=__outer_tests . /deps/__outer_tests/tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
'name = "tests"' \\
|
||||
'version = "0.1"' \\
|
||||
'[tool.setuptools.package-data]' \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||
"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {
|
||||
"__outer_tests": str(pathlib.Path(__file__).parent.parent.absolute()),
|
||||
}
|
||||
|
||||
|
||||
def test_config_to_docker_pipconfig():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
actual_docker_stdin = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -286,7 +216,6 @@ def test_config_to_docker_pipconfig():
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api:3.11
|
||||
ADD pipconfig.txt /pipconfig.txt
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -296,15 +225,11 @@ RUN set -ex && \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||
"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
def test_config_to_docker_invalid_inputs():
|
||||
@@ -329,7 +254,7 @@ def test_config_to_docker_invalid_inputs():
|
||||
|
||||
def test_config_to_docker_local_deps():
|
||||
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
actual_docker_stdin = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -341,7 +266,6 @@ def test_config_to_docker_local_deps():
|
||||
)
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api-custom:3.11
|
||||
# -- Adding non-package dependency graphs --
|
||||
ADD ./graphs /deps/__outer_graphs/src
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -351,14 +275,10 @@ RUN set -ex && \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency graphs --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'\
|
||||
"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
def test_config_to_docker_pyproject():
|
||||
@@ -371,7 +291,7 @@ dependencies = ["langchain"]"""
|
||||
f.write(pyproject_str)
|
||||
|
||||
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
actual_docker_stdin = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -383,21 +303,16 @@ dependencies = ["langchain"]"""
|
||||
)
|
||||
os.remove(pyproject_path)
|
||||
expected_docker_stdin = """FROM langchain/langgraph-api:3.11
|
||||
# -- Adding local package . --
|
||||
ADD . /deps/unit_tests
|
||||
# -- End of local package . --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}'
|
||||
WORKDIR /deps/unit_tests"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
def test_config_to_docker_end_to_end():
|
||||
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
actual_docker_stdin = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -415,7 +330,6 @@ ARG meow
|
||||
ARG foo
|
||||
ADD pipconfig.txt /pipconfig.txt
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain langchain_openai
|
||||
# -- Adding non-package dependency graphs --
|
||||
ADD ./graphs/ /deps/__outer_graphs/src
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -425,19 +339,15 @@ RUN set -ex && \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency graphs --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
# node.js build used for LangGraph Cloud
|
||||
def test_config_to_docker_nodejs():
|
||||
graphs = {"agent": "./graphs/agent.js:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
actual_docker_stdin = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -458,7 +368,6 @@ WORKDIR /deps/unit_tests
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
|
||||
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
# config_to_compose
|
||||
@@ -471,7 +380,6 @@ def test_config_to_compose_simple_config():
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -481,10 +389,7 @@ def test_config_to_compose_simple_config():
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
"""
|
||||
@@ -505,7 +410,6 @@ def test_config_to_compose_env_vars():
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api-custom:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -515,10 +419,7 @@ def test_config_to_compose_env_vars():
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
"""
|
||||
@@ -546,7 +447,6 @@ def test_config_to_compose_env_file():
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -556,10 +456,7 @@ def test_config_to_compose_env_file():
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
"""
|
||||
@@ -580,7 +477,6 @@ def test_config_to_compose_watch():
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -590,10 +486,7 @@ def test_config_to_compose_watch():
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
|
||||
@@ -623,7 +516,6 @@ def test_config_to_compose_end_to_end():
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -633,10 +525,7 @@ def test_config_to_compose_end_to_end():
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.runnables.config import var_child_runnable_config
|
||||
@@ -10,10 +9,6 @@ from langgraph.store.base import BaseStore
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
|
||||
def _no_op_stream_writer(c: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def get_config() -> RunnableConfig:
|
||||
if sys.version_info < (3, 11):
|
||||
try:
|
||||
@@ -182,4 +177,4 @@ def get_stream_writer() -> StreamWriter:
|
||||
```
|
||||
"""
|
||||
config = get_config()
|
||||
return config[CONF].get(CONFIG_KEY_STREAM_WRITER, _no_op_stream_writer)
|
||||
return config[CONF][CONFIG_KEY_STREAM_WRITER]
|
||||
|
||||
@@ -81,8 +81,6 @@ CONFIG_KEY_SCRATCHPAD = sys.intern("__pregel_scratchpad")
|
||||
# holds a mutable dict for temporary storage scoped to the current task
|
||||
CONFIG_KEY_PREVIOUS = sys.intern("__pregel_previous")
|
||||
# holds the previous return value from a stateful Pregel graph.
|
||||
CONFIG_KEY_RUNNER_SUBMIT = sys.intern("__pregel_runner_submit")
|
||||
# holds a function that receives tasks from runner, executes them and returns results
|
||||
|
||||
# --- Other constants ---
|
||||
PUSH = sys.intern("__pregel_push")
|
||||
|
||||
@@ -57,6 +57,9 @@ def task(
|
||||
]:
|
||||
"""Define a LangGraph task using the `task` decorator.
|
||||
|
||||
!!! warning "Beta"
|
||||
The Functional API is currently in beta and is subject to change.
|
||||
|
||||
!!! important "Requires python 3.11 or higher for async functions"
|
||||
The `task` decorator supports both sync and async functions. To use async
|
||||
functions, ensure that you are using Python 3.11 or higher.
|
||||
@@ -150,6 +153,10 @@ S = TypeVar("S")
|
||||
class entrypoint:
|
||||
"""Define a LangGraph workflow using the `entrypoint` decorator.
|
||||
|
||||
!!! warning "Beta"
|
||||
The Functional API is currently in beta and is subject to change.
|
||||
|
||||
|
||||
### Function signature
|
||||
|
||||
The decorated function must accept a **single parameter**, which serves as the input
|
||||
|
||||
@@ -286,7 +286,7 @@ class StateGraph(Graph):
|
||||
Will take the name of the function/runnable as the node name.
|
||||
|
||||
Args:
|
||||
node (Union[str, RunnableLike]): The function or runnable this node will run.
|
||||
node (Union[str, RunnableLike)]: The function or runnable this node will run.
|
||||
action (Optional[RunnableLike]): The action associated with the node. (default: None)
|
||||
metadata (Optional[dict[str, Any]]): The metadata associated with the node. (default: None)
|
||||
input (Optional[Type[Any]]): The input schema for the node. (default: the graph's input schema)
|
||||
|
||||
@@ -12,11 +12,7 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from langchain_core.language_models import (
|
||||
BaseChatModel,
|
||||
LanguageModelInput,
|
||||
LanguageModelLike,
|
||||
)
|
||||
from langchain_core.language_models import BaseChatModel, LanguageModelLike
|
||||
from langchain_core.messages import AIMessage, BaseMessage, SystemMessage, ToolMessage
|
||||
from langchain_core.runnables import (
|
||||
Runnable,
|
||||
@@ -67,15 +63,15 @@ PROMPT_RUNNABLE_NAME = "Prompt"
|
||||
MessagesModifier = Union[
|
||||
SystemMessage,
|
||||
str,
|
||||
Callable[[Sequence[BaseMessage]], LanguageModelInput],
|
||||
Runnable[Sequence[BaseMessage], LanguageModelInput],
|
||||
Callable[[Sequence[BaseMessage]], Sequence[BaseMessage]],
|
||||
Runnable[Sequence[BaseMessage], Sequence[BaseMessage]],
|
||||
]
|
||||
|
||||
Prompt = Union[
|
||||
SystemMessage,
|
||||
str,
|
||||
Callable[[StateSchema], LanguageModelInput],
|
||||
Runnable[StateSchema, LanguageModelInput],
|
||||
Callable[[StateSchema], Sequence[BaseMessage]],
|
||||
Runnable[StateSchema, Sequence[BaseMessage]],
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ from langgraph.constants import (
|
||||
CONFIG_KEY_NODE_FINISHED,
|
||||
CONFIG_KEY_READ,
|
||||
CONFIG_KEY_RESUMING,
|
||||
CONFIG_KEY_RUNNER_SUBMIT,
|
||||
CONFIG_KEY_SEND,
|
||||
CONFIG_KEY_STORE,
|
||||
CONFIG_KEY_STREAM,
|
||||
@@ -250,6 +249,25 @@ class Pregel(PregelProtocol):
|
||||
|
||||
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]
|
||||
@@ -1737,7 +1755,7 @@ class Pregel(PregelProtocol):
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
submit=config[CONF].get(CONFIG_KEY_RUNNER_SUBMIT, loop.submit),
|
||||
submit=loop.submit,
|
||||
put_writes=loop.put_writes,
|
||||
schedule_task=loop.accept_push,
|
||||
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
|
||||
@@ -2029,7 +2047,7 @@ class Pregel(PregelProtocol):
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
submit=config[CONF].get(CONFIG_KEY_RUNNER_SUBMIT, loop.submit),
|
||||
submit=loop.submit,
|
||||
put_writes=loop.put_writes,
|
||||
schedule_task=loop.accept_push,
|
||||
use_astream=do_stream is not None,
|
||||
|
||||
@@ -323,15 +323,11 @@ class RemoteGraph(PregelProtocol):
|
||||
if k not in reserved_configurable_keys and not k.startswith("__pregel_")
|
||||
}
|
||||
|
||||
sanitized: RunnableConfig = {
|
||||
return {
|
||||
"tags": config.get("tags") or [],
|
||||
"metadata": config.get("metadata") or {},
|
||||
"configurable": new_configurable,
|
||||
}
|
||||
if "recursion_limit" in config:
|
||||
sanitized["recursion_limit"] = config["recursion_limit"]
|
||||
|
||||
return sanitized
|
||||
|
||||
def get_state(
|
||||
self, config: RunnableConfig, *, subgraphs: bool = False
|
||||
|
||||
@@ -13,7 +13,6 @@ from typing import (
|
||||
Coroutine,
|
||||
Iterator,
|
||||
Optional,
|
||||
Protocol,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
@@ -36,7 +35,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 TypeGuard
|
||||
from typing_extensions import Concatenate, ParamSpec, TypeGuard
|
||||
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
@@ -133,51 +132,12 @@ Each tuple contains:
|
||||
VALID_KINDS = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
||||
|
||||
|
||||
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: ...
|
||||
|
||||
P = ParamSpec("P") # to handle injected kwargs like `writer` / `store`
|
||||
|
||||
RunnableLike = Union[
|
||||
LCRunnableLike,
|
||||
_RunnableWithWriter[Input, Output],
|
||||
_RunnableWithStore[Input, Output],
|
||||
_RunnableWithWriterStore[Input, Output],
|
||||
_RunnableWithConfigWriter[Input, Output],
|
||||
_RunnableWithConfigStore[Input, Output],
|
||||
_RunnableWithConfigWriterStore[Input, Output],
|
||||
Callable[Concatenate[Input, P], Output],
|
||||
Callable[Concatenate[Input, P], Awaitable[Output]],
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.74"
|
||||
version = "0.2.72"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -133,6 +133,8 @@ async def test_invoke_two_processes_in_out_interrupt(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 6,
|
||||
@@ -156,6 +158,8 @@ async def test_invoke_two_processes_in_out_interrupt(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 5,
|
||||
@@ -179,6 +183,8 @@ async def test_invoke_two_processes_in_out_interrupt(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"step": 4,
|
||||
@@ -200,6 +206,8 @@ async def test_invoke_two_processes_in_out_interrupt(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 3,
|
||||
@@ -223,6 +231,8 @@ async def test_invoke_two_processes_in_out_interrupt(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"step": 2,
|
||||
@@ -244,6 +254,8 @@ async def test_invoke_two_processes_in_out_interrupt(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -267,6 +279,8 @@ async def test_invoke_two_processes_in_out_interrupt(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 0,
|
||||
@@ -290,6 +304,7 @@ async def test_invoke_two_processes_in_out_interrupt(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"step": -1,
|
||||
@@ -366,6 +381,8 @@ async def test_fork_always_re_runs_nodes(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 5,
|
||||
@@ -387,6 +404,8 @@ async def test_fork_always_re_runs_nodes(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 4,
|
||||
@@ -408,6 +427,8 @@ async def test_fork_always_re_runs_nodes(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 3,
|
||||
@@ -429,6 +450,8 @@ async def test_fork_always_re_runs_nodes(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 2,
|
||||
@@ -450,6 +473,8 @@ async def test_fork_always_re_runs_nodes(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -471,6 +496,8 @@ async def test_fork_always_re_runs_nodes(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 0,
|
||||
@@ -494,6 +521,7 @@ async def test_fork_always_re_runs_nodes(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"step": -1,
|
||||
@@ -834,6 +862,8 @@ 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,
|
||||
@@ -894,6 +924,8 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 1,
|
||||
@@ -1018,6 +1050,8 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 4,
|
||||
@@ -1100,6 +1134,8 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 0,
|
||||
@@ -1160,6 +1196,8 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 1,
|
||||
@@ -1284,6 +1322,8 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 4,
|
||||
@@ -1366,6 +1406,8 @@ async def test_conditional_graph(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 0,
|
||||
@@ -1798,6 +1840,8 @@ async def test_conditional_graph_state(
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -1853,6 +1897,8 @@ async def test_conditional_graph_state(
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 2,
|
||||
@@ -1943,6 +1989,8 @@ async def test_conditional_graph_state(
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 5,
|
||||
@@ -2010,6 +2058,8 @@ async def test_conditional_graph_state(
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -2064,6 +2114,8 @@ async def test_conditional_graph_state(
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 2,
|
||||
@@ -2152,6 +2204,8 @@ async def test_conditional_graph_state(
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 5,
|
||||
@@ -2768,6 +2822,8 @@ async def test_state_graph_packets(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -2827,6 +2883,8 @@ 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,
|
||||
@@ -2936,6 +2994,8 @@ 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,
|
||||
@@ -3005,6 +3065,8 @@ 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,
|
||||
@@ -3079,6 +3141,8 @@ 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,
|
||||
@@ -3140,6 +3204,8 @@ 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,
|
||||
@@ -3249,6 +3315,8 @@ 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,
|
||||
@@ -3318,6 +3386,8 @@ 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,
|
||||
@@ -3575,6 +3645,8 @@ 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,
|
||||
@@ -3629,6 +3701,8 @@ 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,
|
||||
@@ -3719,6 +3793,8 @@ 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,
|
||||
@@ -3779,6 +3855,8 @@ 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,
|
||||
@@ -4087,6 +4165,8 @@ 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,
|
||||
@@ -4095,6 +4175,7 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
},
|
||||
{
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"step": -1,
|
||||
@@ -4117,6 +4198,8 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 0,
|
||||
@@ -4150,6 +4233,8 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -4185,6 +4270,8 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 0,
|
||||
@@ -4218,6 +4305,8 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -4253,6 +4342,8 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 0,
|
||||
@@ -4283,6 +4374,8 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 1,
|
||||
@@ -4316,6 +4409,8 @@ async def test_start_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 2,
|
||||
@@ -4856,6 +4951,8 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -4888,6 +4985,8 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 3,
|
||||
@@ -4922,6 +5021,8 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -4954,6 +5055,8 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 3,
|
||||
@@ -4996,6 +5099,8 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -5028,6 +5133,8 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 3,
|
||||
@@ -5062,6 +5169,8 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -5094,6 +5203,8 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 3,
|
||||
@@ -5122,6 +5233,7 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
config=uconfig,
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 0,
|
||||
@@ -5149,6 +5261,8 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
@@ -5175,6 +5289,8 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 3,
|
||||
@@ -5266,6 +5382,8 @@ 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"}},
|
||||
@@ -5317,6 +5435,8 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict({"": AnyStr()}),
|
||||
"parents": {
|
||||
"": AnyStr(),
|
||||
},
|
||||
@@ -5363,6 +5483,8 @@ 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"}},
|
||||
@@ -5409,6 +5531,8 @@ 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"}},
|
||||
@@ -5447,6 +5571,8 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": None,
|
||||
@@ -5481,6 +5607,7 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"writes": {"__start__": {"my_key": "my value"}},
|
||||
@@ -5516,6 +5643,8 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict({"": AnyStr()}),
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
"inner_1": {
|
||||
@@ -5564,6 +5693,8 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict({"": AnyStr()}),
|
||||
"source": "loop",
|
||||
"writes": None,
|
||||
"step": 0,
|
||||
@@ -5613,6 +5744,8 @@ 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,
|
||||
@@ -5658,6 +5791,8 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
@@ -5694,6 +5829,8 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
@@ -5736,6 +5873,8 @@ 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"}},
|
||||
@@ -5776,6 +5915,8 @@ 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"}},
|
||||
@@ -5810,6 +5951,8 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": None,
|
||||
@@ -5844,6 +5987,7 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"writes": {"__start__": {"my_key": "my value"}},
|
||||
@@ -5953,6 +6097,8 @@ 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"}},
|
||||
@@ -5999,6 +6145,8 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {"": AnyStr()},
|
||||
"source": "loop",
|
||||
"writes": None,
|
||||
@@ -6045,6 +6193,8 @@ 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(),
|
||||
@@ -6124,6 +6274,10 @@ 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(),
|
||||
@@ -6186,6 +6340,8 @@ 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,
|
||||
@@ -6225,6 +6381,8 @@ 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"}},
|
||||
@@ -6273,6 +6431,8 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
@@ -6318,6 +6478,8 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
@@ -6348,6 +6510,8 @@ 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"}},
|
||||
@@ -6392,6 +6556,8 @@ 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"}},
|
||||
@@ -6449,6 +6615,7 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"writes": {"my_key": "my value"},
|
||||
@@ -6484,6 +6651,8 @@ 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,
|
||||
@@ -6523,6 +6692,8 @@ 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,
|
||||
@@ -6575,6 +6746,8 @@ 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,
|
||||
@@ -6622,6 +6795,10 @@ 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"}
|
||||
@@ -6679,6 +6856,10 @@ 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,
|
||||
@@ -6741,6 +6922,10 @@ 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,
|
||||
@@ -6803,6 +6988,10 @@ 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,
|
||||
@@ -7081,6 +7270,8 @@ async def test_weather_subgraph(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"source": "loop",
|
||||
"writes": {"router_node": {"route": "weather"}},
|
||||
"step": 1,
|
||||
@@ -7177,6 +7368,8 @@ async def test_weather_subgraph(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"source": "loop",
|
||||
"writes": {"router_node": {"route": "weather"}},
|
||||
"step": 1,
|
||||
@@ -7222,6 +7415,8 @@ async def test_weather_subgraph(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict({"": AnyStr()}),
|
||||
"source": "loop",
|
||||
"writes": {"model_node": {"city": "San Francisco"}},
|
||||
"step": 1,
|
||||
@@ -7285,6 +7480,8 @@ async def test_weather_subgraph(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"source": "loop",
|
||||
"writes": {"router_node": {"route": "weather"}},
|
||||
"step": 1,
|
||||
@@ -7331,6 +7528,7 @@ async def test_weather_subgraph(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_map": AnyDict({"": AnyStr()}),
|
||||
"step": 2,
|
||||
"source": "update",
|
||||
"writes": {
|
||||
|
||||
@@ -63,6 +63,7 @@ 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,
|
||||
@@ -182,6 +183,8 @@ 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")]}},
|
||||
@@ -214,6 +217,8 @@ 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")]}},
|
||||
|
||||
@@ -54,7 +54,6 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointTuple,
|
||||
)
|
||||
from langgraph.checkpoint.memory import InMemorySaver, MemorySaver
|
||||
from langgraph.config import get_stream_writer
|
||||
from langgraph.constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL, START
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.func import entrypoint, task
|
||||
@@ -1113,6 +1112,8 @@ 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,
|
||||
@@ -1211,6 +1212,8 @@ def test_pending_writes_resume(
|
||||
"channel_values": {"one": "one", "two": "two", "value": 6},
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"step": 1,
|
||||
"source": "loop",
|
||||
@@ -1261,6 +1264,8 @@ def test_pending_writes_resume(
|
||||
},
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"step": 0,
|
||||
"source": "loop",
|
||||
@@ -1302,6 +1307,7 @@ def test_pending_writes_resume(
|
||||
"channel_values": {"__start__": {"value": 1}},
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"step": -1,
|
||||
"source": "input",
|
||||
@@ -2392,6 +2398,8 @@ def test_in_one_fan_out_state_graph_waiting_edge(
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "update",
|
||||
"step": 4,
|
||||
@@ -4827,6 +4835,8 @@ def test_parent_command(request: pytest.FixtureRequest, checkpointer_name: str)
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
"alice": {
|
||||
@@ -6496,34 +6506,3 @@ def test_pydantic_none_state_update() -> None:
|
||||
|
||||
graph = StateGraph(State).add_node(node_a).add_edge(START, "node_a").compile()
|
||||
assert graph.invoke({"foo": ""}) == {"foo": None}
|
||||
|
||||
|
||||
def test_get_stream_writer() -> None:
|
||||
class State(TypedDict):
|
||||
foo: str
|
||||
|
||||
def my_node(state):
|
||||
writer = get_stream_writer()
|
||||
writer("custom!")
|
||||
return state
|
||||
|
||||
graph = StateGraph(State).add_node(my_node).add_edge(START, "my_node").compile()
|
||||
assert list(graph.stream({"foo": "bar"}, stream_mode="custom")) == ["custom!"]
|
||||
assert list(graph.stream({"foo": "bar"}, stream_mode="values")) == [
|
||||
{"foo": "bar"},
|
||||
{"foo": "bar"},
|
||||
]
|
||||
assert list(graph.stream({"foo": "bar"}, stream_mode=["custom", "updates"])) == [
|
||||
(
|
||||
"custom",
|
||||
"custom!",
|
||||
),
|
||||
(
|
||||
"updates",
|
||||
{
|
||||
"my_node": {
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -606,6 +606,8 @@ 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,
|
||||
@@ -613,6 +615,7 @@ async def test_dynamic_interrupt(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
},
|
||||
{
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"step": -1,
|
||||
@@ -641,6 +644,8 @@ 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,
|
||||
@@ -667,6 +672,8 @@ 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,
|
||||
@@ -791,6 +798,8 @@ 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,
|
||||
@@ -798,6 +807,7 @@ async def test_dynamic_interrupt_subgraph(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
},
|
||||
{
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"step": -1,
|
||||
@@ -832,6 +842,8 @@ 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,
|
||||
@@ -858,6 +870,8 @@ 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,
|
||||
@@ -976,6 +990,8 @@ 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,
|
||||
@@ -983,6 +999,7 @@ async def test_copy_checkpoint(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
},
|
||||
{
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "input",
|
||||
"step": -1,
|
||||
@@ -1021,6 +1038,8 @@ 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,
|
||||
@@ -1064,6 +1083,8 @@ 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,
|
||||
@@ -1230,6 +1251,8 @@ 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,
|
||||
@@ -1307,6 +1330,8 @@ 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,
|
||||
@@ -2007,6 +2032,8 @@ async def test_pending_writes_resume(
|
||||
),
|
||||
)
|
||||
assert state.metadata == {
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"step": 0,
|
||||
@@ -2105,6 +2132,8 @@ 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",
|
||||
@@ -2157,6 +2186,8 @@ async def test_pending_writes_resume(
|
||||
},
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"step": 0,
|
||||
"source": "loop",
|
||||
@@ -2200,6 +2231,7 @@ async def test_pending_writes_resume(
|
||||
"channel_values": {"__start__": {"value": 1}},
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_ns": "",
|
||||
"parents": {},
|
||||
"step": -1,
|
||||
"source": "input",
|
||||
@@ -2778,6 +2810,8 @@ 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",
|
||||
@@ -2814,6 +2848,8 @@ 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",
|
||||
@@ -2857,6 +2893,8 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
"2": [
|
||||
@@ -2922,6 +2960,8 @@ 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",
|
||||
@@ -2977,6 +3017,8 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"source": "loop",
|
||||
"writes": None,
|
||||
"thread_id": "1",
|
||||
@@ -3014,6 +3056,7 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_ns": "",
|
||||
"source": "input",
|
||||
"writes": {"__start__": ["0"]},
|
||||
"thread_id": "1",
|
||||
@@ -3186,6 +3229,8 @@ async def test_send_react_interrupt(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"step": 1,
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
@@ -3257,6 +3302,8 @@ async def test_send_react_interrupt(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"step": 2,
|
||||
"source": "update",
|
||||
"writes": {
|
||||
@@ -3344,6 +3391,8 @@ async def test_send_react_interrupt(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"step": 1,
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
@@ -3436,6 +3485,8 @@ async def test_send_react_interrupt(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"step": 2,
|
||||
"source": "update",
|
||||
"writes": {
|
||||
@@ -3651,6 +3702,8 @@ async def test_send_react_interrupt_control(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"step": 1,
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
@@ -3722,6 +3775,8 @@ async def test_send_react_interrupt_control(
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"step": 2,
|
||||
"source": "update",
|
||||
"writes": {
|
||||
@@ -4671,6 +4726,8 @@ 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"}},
|
||||
@@ -6145,6 +6202,8 @@ async def test_parent_command(checkpointer_name: str) -> None:
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_ns": "",
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
"alice": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.52"
|
||||
version = "0.1.51"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user