mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-28 18:59:42 +02:00
Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
277c341817 | ||
|
|
88d0f41c55 | ||
|
|
0a4953c4bc | ||
|
|
059e16789c | ||
|
|
41b36dcf3f | ||
|
|
b39dcd7fad | ||
|
|
c0ad92b6db | ||
|
|
b8d25bc0ed | ||
|
|
7884401ec8 | ||
|
|
a3bc029344 | ||
|
|
4875973ac5 | ||
|
|
15e67fdd57 | ||
|
|
1393270664 | ||
|
|
6e0295b4de | ||
|
|
4aadfccf95 | ||
|
|
70b2da1301 | ||
|
|
209864da45 | ||
|
|
8b6ef35f0c | ||
|
|
a01537d1a5 | ||
|
|
05b4a30c04 | ||
|
|
93e10fbe15 | ||
|
|
f6989f2c7d | ||
|
|
c4f8346479 | ||
|
|
82c9d4b368 | ||
|
|
647f22fdd9 | ||
|
|
0aba1b4887 | ||
|
|
4c0c52d996 | ||
|
|
580fe68c8e | ||
|
|
2f26268ff4 | ||
|
|
84f8a43f6e | ||
|
|
7ce0e3e15e | ||
|
|
1e6d958434 | ||
|
|
68ba8aa393 | ||
|
|
e6a0f08561 | ||
|
|
f38784a291 | ||
|
|
fc834127fd | ||
|
|
f3a0cbf294 | ||
|
|
f4fec76257 | ||
|
|
457641f15e | ||
|
|
9f15e15e26 | ||
|
|
cb9989030e | ||
|
|
a008725c06 | ||
|
|
a93f17e624 | ||
|
|
c7eddcc6e3 | ||
|
|
4cdad6c206 | ||
|
|
b9fb155d59 | ||
|
|
8b817a5b16 | ||
|
|
3a67f3a3eb | ||
|
|
1283539500 | ||
|
|
69ad42cac5 | ||
|
|
264b02e3ad | ||
|
|
b5c659bc9f | ||
|
|
4623f7b5da | ||
|
|
1641402341 | ||
|
|
d03ead2f43 | ||
|
|
5a8624fdfd |
@@ -20,7 +20,30 @@ env:
|
||||
POETRY_VERSION: "1.7.1"
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
python: ${{ steps.filter.outputs.python }}
|
||||
sdk-js: ${{ steps.filter.outputs.sdk-js }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
python:
|
||||
- 'libs/langgraph/**'
|
||||
- 'libs/sdk-py/**'
|
||||
- 'libs/cli/**'
|
||||
- 'libs/checkpoint/**'
|
||||
- 'libs/checkpoint-sqlite/**'
|
||||
- 'libs/checkpoint-postgres/**'
|
||||
- 'libs/scheduler-kafka/**'
|
||||
sdk-js:
|
||||
- 'libs/sdk-js/**'
|
||||
|
||||
lint:
|
||||
needs: changes
|
||||
name: cd ${{ matrix.working-directory }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -34,12 +57,14 @@ jobs:
|
||||
"libs/checkpoint-postgres",
|
||||
"libs/scheduler-kafka",
|
||||
]
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
uses: ./.github/workflows/_lint.yml
|
||||
with:
|
||||
working-directory: ${{ matrix.working-directory }}
|
||||
secrets: inherit
|
||||
|
||||
test:
|
||||
needs: changes
|
||||
name: cd ${{ matrix.working-directory }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -50,6 +75,7 @@ jobs:
|
||||
"libs/checkpoint-sqlite",
|
||||
"libs/checkpoint-postgres",
|
||||
]
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
uses: ./.github/workflows/_test.yml
|
||||
with:
|
||||
working-directory: ${{ matrix.working-directory }}
|
||||
@@ -57,17 +83,23 @@ jobs:
|
||||
|
||||
# NOTE: we're testing langgraph separately because it requires a different matrix
|
||||
test-langgraph:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: "cd libs/langgraph"
|
||||
uses: ./.github/workflows/_test_langgraph.yml
|
||||
secrets: inherit
|
||||
|
||||
# NOTE: we're testing scheduler-kafka separately because it requires a different matrix
|
||||
test-scheduler-kafka:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: "cd libs/scheduler-kafka"
|
||||
uses: ./.github/workflows/_test_scheduler_kafka.yml
|
||||
secrets: inherit
|
||||
|
||||
check-sdk-methods:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: "Check SDK methods matching"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -80,11 +112,15 @@ jobs:
|
||||
run: python .github/scripts/check_sdk_methods.py
|
||||
|
||||
integration-test:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
name: CLI integration test
|
||||
uses: ./.github/workflows/_integration_test.yml
|
||||
secrets: inherit
|
||||
|
||||
lint-js:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.sdk-js == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -109,6 +145,8 @@ jobs:
|
||||
run: yarn build
|
||||
|
||||
test-js:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.sdk-js == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
+1
-7
@@ -26,15 +26,9 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-unused-vars
|
||||
class HashedCassette {
|
||||
private recording = true;
|
||||
|
||||
constructor(
|
||||
private readonly cassettePath: string,
|
||||
private readonly hash: string
|
||||
) {}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
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 as e:
|
||||
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 as e:
|
||||
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
|
||||
@@ -1,12 +1,9 @@
|
||||
import ast
|
||||
import importlib
|
||||
from importlib.machinery import ModuleSpec
|
||||
import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
import re
|
||||
from functools import lru_cache
|
||||
import sys
|
||||
from typing import List, Literal, Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
@@ -42,7 +39,6 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
|
||||
(["langgraph.graph"], "langgraph.graph.message", "add_messages", "graphs"),
|
||||
(["langgraph.graph"], "langgraph.graph.state", "StateGraph", "graphs"),
|
||||
(["langgraph.graph"], "langgraph.graph.state", "CompiledStateGraph", "graphs"),
|
||||
([], "langgraph.types", "StreamMode", "types"),
|
||||
(["langgraph.graph"], "langgraph.constants", "START", "constants"),
|
||||
(["langgraph.graph"], "langgraph.constants", "END", "constants"),
|
||||
(["langgraph.constants"], "langgraph.types", "Send", "types"),
|
||||
@@ -51,7 +47,9 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
|
||||
(["langgraph.constants"], "langgraph.types", "Command", "types"),
|
||||
(["langgraph.func"], "langgraph.func", "entrypoint", "func"),
|
||||
(["langgraph.func"], "langgraph.func", "task", "func"),
|
||||
([], "langgraph.types", "RetryPolicy", "types"),
|
||||
(["langgraph.types"], "langgraph.types", "RetryPolicy", "types"),
|
||||
(["langgraph.types"], "langgraph.types", "StreamMode", "types"),
|
||||
(["langgraph.types"], "langgraph.types", "StreamWriter", "types"),
|
||||
([], "langgraph.checkpoint.base", "Checkpoint", "checkpoints"),
|
||||
([], "langgraph.checkpoint.base", "CheckpointMetadata", "checkpoints"),
|
||||
([], "langgraph.checkpoint.base", "BaseCheckpointSaver", "checkpoints"),
|
||||
@@ -71,224 +69,149 @@ WELL_KNOWN_LANGGRAPH_OBJECTS = {
|
||||
}
|
||||
|
||||
|
||||
def _make_regular_expression(pkg_prefix: str) -> re.Pattern:
|
||||
if not pkg_prefix.isidentifier():
|
||||
raise ValueError(f"Invalid package prefix: {pkg_prefix}")
|
||||
return re.compile(
|
||||
r"from\s+(" + pkg_prefix + r"(?:_\w+)?(?:\.\w+)*?)\s+import\s+\(?"
|
||||
r"((?:\w+(?:,\s*)?)*)\s*\)?", # Match zero or more words separated by a comma+optional ws
|
||||
re.DOTALL, # Match newlines as well
|
||||
)
|
||||
|
||||
|
||||
# Regular expression to match langchain import lines
|
||||
_IMPORT_LANGCHAIN_RE = _make_regular_expression("langchain")
|
||||
_IMPORT_LANGGRAPH_RE = _make_regular_expression("langgraph")
|
||||
|
||||
|
||||
@lru_cache(maxsize=10_000)
|
||||
def _get_full_module_name(
|
||||
module_path: str, class_name: str | None, doc_title: str
|
||||
) -> Optional[str]:
|
||||
def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
|
||||
"""Get full module name using inspect, with LRU cache to memoize results."""
|
||||
try:
|
||||
if module_path in sys.modules:
|
||||
module = sys.modules[module_path]
|
||||
else:
|
||||
spec: ModuleSpec | None = importlib.util.find_spec(module_path)
|
||||
if spec is not None:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_path] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
if class_name is not None:
|
||||
class_ = getattr(module, class_name)
|
||||
|
||||
if re.match(r"\w+\s+as\s+\w+", class_name):
|
||||
# Handle cases like "A as B"
|
||||
class_name, _ = class_name.split(" as ")
|
||||
|
||||
module = inspect.getmodule(class_)
|
||||
if module is None:
|
||||
# For constants, inspect.getmodule() might return None
|
||||
# In this case, we'll return the original module_path
|
||||
return module_path
|
||||
return module.__name__
|
||||
module = importlib.import_module(module_path)
|
||||
symbol = getattr(module, class_name)
|
||||
# First check the __module__ attribute on the symbol.
|
||||
mod_name = getattr(symbol, "__module__", None)
|
||||
# If __module__ is not set or comes from typing,
|
||||
# assume the definition is in module_path.
|
||||
if mod_name is None or mod_name.startswith("typing"):
|
||||
return module_path
|
||||
return mod_name
|
||||
except AttributeError as e:
|
||||
if class_name is not None:
|
||||
# the class_name might actually be a module
|
||||
# e.g. from langchain import hub
|
||||
# try to import it as a module, and if that doesn't work, throw
|
||||
if class_name is not None:
|
||||
module_name = _get_full_module_name(
|
||||
f"{module_path}.{class_name}", None, doc_title
|
||||
)
|
||||
if module_name is not None:
|
||||
# return the name of the parent module, rather than the name of the class as though it were a module
|
||||
return module.__name__
|
||||
logger.warning(
|
||||
f"API Reference: Could not find module for {class_name} in {module_path}, imported in doc {doc_title}, {e}"
|
||||
)
|
||||
# don't log if we're trying to import the "hub" part as though it were a module
|
||||
logger.warning(
|
||||
f"API Reference: Could not find module for {module_path}, imported in doc {doc_title}, {e}"
|
||||
)
|
||||
logger.warning(f"API Reference: Could not find module for {class_name}, {e}")
|
||||
return None
|
||||
except ImportError as e:
|
||||
logger.warning(
|
||||
f"API Reference: Failed to import module {module_path} {doc_title}, {e}"
|
||||
)
|
||||
logger.warning(f"API Reference: Failed to load for class {class_name}, {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _get_doc_title(data: str, file_name: str) -> str:
|
||||
try:
|
||||
return re.findall(r"^#\s*(.*)", data, re.MULTILINE)[0]
|
||||
except IndexError:
|
||||
pass
|
||||
# Parse the rst-style titles
|
||||
try:
|
||||
return re.findall(r"^(.*)\n=+\n", data, re.MULTILINE)[0]
|
||||
except IndexError:
|
||||
return file_name
|
||||
|
||||
|
||||
class ImportInformation(TypedDict):
|
||||
imported: str # The name of the class that was imported.
|
||||
source: str # The full module path from which the class was imported.
|
||||
docs: str # The URL pointing to the class's documentation.
|
||||
title: str # The title of the document where the import is used.
|
||||
path: str # The path of the file where the markdown content originated.
|
||||
|
||||
|
||||
def _get_imports(
|
||||
code: str, doc_title: str, package_ecosystem: Literal["langchain", "langgraph"]
|
||||
) -> List[ImportInformation]:
|
||||
"""Get imports from the given code block.
|
||||
|
||||
Args:
|
||||
code: Python code block from which to extract imports
|
||||
doc_title: Title of the document
|
||||
package_ecosystem: "langchain" or "langgraph". The two live in different
|
||||
repositories and have separate documentation sites.
|
||||
|
||||
Returns:
|
||||
List of import information for the given code block
|
||||
"""
|
||||
imports = []
|
||||
|
||||
if package_ecosystem == "langchain":
|
||||
pattern = _IMPORT_LANGCHAIN_RE
|
||||
elif package_ecosystem == "langgraph":
|
||||
pattern = _IMPORT_LANGGRAPH_RE
|
||||
else:
|
||||
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
|
||||
|
||||
for import_match in pattern.finditer(code):
|
||||
module = import_match.group(1)
|
||||
if "pydantic_v1" in module:
|
||||
continue
|
||||
imports_str = (
|
||||
import_match.group(2).replace("(\n", "").replace("\n)", "")
|
||||
) # Handle newlines within parentheses
|
||||
# remove any newline and spaces, then split by comma
|
||||
imported_classes = [
|
||||
imp.strip()
|
||||
for imp in re.split(r",\s*", imports_str.replace("\n", ""))
|
||||
if imp.strip()
|
||||
]
|
||||
for class_name in imported_classes:
|
||||
if module == "langchain_core.messages" and class_name == ")":
|
||||
print("WARNING: ", file=sys.stderr)
|
||||
print(
|
||||
f"WARNING: Trying to import {class_name} from {module} in doc {doc_title}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("WARNING: ", file=sys.stderr)
|
||||
print("WARNING:", import_match.group(0), file=sys.stderr)
|
||||
print("WARNING: ", file=sys.stderr)
|
||||
print(
|
||||
"\n".join([f"WARNING: {line}" for line in code.splitlines()]),
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("WARNING: ", file=sys.stderr)
|
||||
|
||||
module_path = _get_full_module_name(module, class_name, doc_title)
|
||||
if not module_path:
|
||||
continue
|
||||
if len(module_path.split(".")) < 2:
|
||||
continue
|
||||
|
||||
if package_ecosystem == "langchain":
|
||||
pkg = module_path.split(".")[0].replace("langchain_", "")
|
||||
top_level_mod = module_path.split(".")[1]
|
||||
|
||||
url = (
|
||||
_LANGCHAIN_API_REFERENCE
|
||||
+ pkg
|
||||
+ "/"
|
||||
+ top_level_mod
|
||||
+ "/"
|
||||
+ module_path
|
||||
+ "."
|
||||
+ class_name
|
||||
+ ".html"
|
||||
)
|
||||
elif package_ecosystem == "langgraph":
|
||||
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
|
||||
# Likely not documented yet
|
||||
continue
|
||||
|
||||
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
|
||||
(module, class_name)
|
||||
]
|
||||
url = (
|
||||
_LANGGRAPH_API_REFERENCE
|
||||
+ namespace
|
||||
+ "/#"
|
||||
+ source_module
|
||||
+ "."
|
||||
+ class_name
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
|
||||
|
||||
# Add the import information to our list
|
||||
imports.append(
|
||||
{
|
||||
"imported": class_name,
|
||||
"source": module,
|
||||
"docs": url,
|
||||
"title": doc_title,
|
||||
}
|
||||
)
|
||||
|
||||
return imports
|
||||
|
||||
|
||||
def get_imports(code: str, doc_title: str) -> List[ImportInformation]:
|
||||
def get_imports(code: str, path: str) -> List[ImportInformation]:
|
||||
"""Retrieve all import references from the given code for specified ecosystems.
|
||||
|
||||
Args:
|
||||
code: The source code from which to extract import references.
|
||||
doc_title: The documentation title associated with the code.
|
||||
path: The path of the file where the markdown content originated.
|
||||
|
||||
Returns:
|
||||
A list of import information for each import found.
|
||||
"""
|
||||
ecosystems = ["langchain", "langgraph"]
|
||||
all_imports = []
|
||||
for package_ecosystem in ecosystems:
|
||||
all_imports.extend(_get_imports(code, doc_title, package_ecosystem))
|
||||
return all_imports
|
||||
# Parse the code into an AST.
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
except SyntaxError:
|
||||
return []
|
||||
|
||||
found_imports = []
|
||||
|
||||
# Walk through the AST and process ImportFrom nodes.
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
# node.module is the source module.
|
||||
if node.module is None:
|
||||
continue
|
||||
for alias in node.names:
|
||||
if not (
|
||||
node.module.startswith("langchain")
|
||||
or node.module.startswith("langgraph")
|
||||
):
|
||||
continue
|
||||
|
||||
found_imports.append(
|
||||
{
|
||||
"source": node.module,
|
||||
# alias.name is the original name even if an alias exists.
|
||||
"imported": alias.name,
|
||||
}
|
||||
)
|
||||
|
||||
imports: list[ImportInformation] = []
|
||||
|
||||
for found_import in found_imports:
|
||||
module = found_import["source"]
|
||||
|
||||
if module.startswith("langchain"):
|
||||
# Handles things like `langchain` or `langchain_anthropic`
|
||||
package_ecosystem = "langchain"
|
||||
elif module.startswith("langgraph"):
|
||||
package_ecosystem = "langgraph"
|
||||
else:
|
||||
continue
|
||||
|
||||
class_name = found_import["imported"]
|
||||
module_path = _get_full_module_name(module, class_name)
|
||||
if not module_path:
|
||||
continue
|
||||
if len(module_path.split(".")) < 2:
|
||||
continue
|
||||
|
||||
if package_ecosystem == "langchain":
|
||||
pkg = module_path.split(".")[0].replace("langchain_", "")
|
||||
top_level_mod = module_path.split(".")[1]
|
||||
|
||||
url = (
|
||||
_LANGCHAIN_API_REFERENCE
|
||||
+ pkg
|
||||
+ "/"
|
||||
+ top_level_mod
|
||||
+ "/"
|
||||
+ module_path
|
||||
+ "."
|
||||
+ class_name
|
||||
+ ".html"
|
||||
)
|
||||
elif package_ecosystem == "langgraph":
|
||||
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
|
||||
# Likely not documented yet
|
||||
continue
|
||||
|
||||
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
|
||||
(module, class_name)
|
||||
]
|
||||
url = (
|
||||
_LANGGRAPH_API_REFERENCE
|
||||
+ namespace
|
||||
+ "/#"
|
||||
+ source_module
|
||||
+ "."
|
||||
+ class_name
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
|
||||
|
||||
# Add the import information to our list
|
||||
imports.append(
|
||||
{
|
||||
"imported": class_name,
|
||||
"source": module,
|
||||
"docs": url,
|
||||
"path": path,
|
||||
}
|
||||
)
|
||||
|
||||
return imports
|
||||
|
||||
|
||||
def update_markdown_with_imports(markdown: str, file_name: str) -> str:
|
||||
def update_markdown_with_imports(markdown: str, path: str) -> str:
|
||||
"""Update markdown to include API reference links for imports in Python code blocks.
|
||||
|
||||
This function scans the markdown content for Python code blocks, extracts any imports, and appends links to their API documentation.
|
||||
This function scans the markdown content for Python code blocks, extracts any
|
||||
imports, and appends links to their API documentation.
|
||||
|
||||
Args:
|
||||
markdown: The markdown content to process.
|
||||
path: The path of the file where the markdown content originated.
|
||||
|
||||
Returns:
|
||||
Updated markdown with API reference links appended to Python code blocks.
|
||||
@@ -299,7 +222,8 @@ def update_markdown_with_imports(markdown: str, file_name: str) -> str:
|
||||
```python
|
||||
from langchain.nlp import TextGenerator
|
||||
```
|
||||
This function will append an API reference link to the `TextGenerator` class from the `langchain.nlp` module if it's recognized.
|
||||
This function will append an API reference link to the `TextGenerator` class
|
||||
from the `langchain.nlp` module if it's recognized.
|
||||
"""
|
||||
code_block_pattern = re.compile(
|
||||
r"(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```",
|
||||
@@ -317,9 +241,8 @@ def update_markdown_with_imports(markdown: str, file_name: str) -> str:
|
||||
"""
|
||||
indent = match.group("indent")
|
||||
code_block = match.group("code")
|
||||
language = match.group("language") # Preserve the language from the regex match
|
||||
# Retrieve import information from the code block
|
||||
imports = get_imports(code_block, file_name)
|
||||
imports = get_imports(code_block, "__unused__")
|
||||
|
||||
original_code_block = match.group(0)
|
||||
# If no imports are found, return the original code block
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import argparse
|
||||
import ast
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Literal, Optional
|
||||
from typing import Literal
|
||||
|
||||
import nbformat
|
||||
from nbconvert.exporters import MarkdownExporter
|
||||
@@ -352,17 +350,6 @@ 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,
|
||||
@@ -372,99 +359,5 @@ def convert_notebook(
|
||||
nb = nbformat.read(f, as_version=4)
|
||||
|
||||
nb.metadata.mode = mode
|
||||
if mode == "markdown":
|
||||
body, _ = exporter.from_notebook_node(nb)
|
||||
else:
|
||||
body, _ = md_executable.from_notebook_node(nb)
|
||||
body, _ = exporter.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,
|
||||
)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"mimetypes": {
|
||||
"text/markdown": true
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{#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 -%}
|
||||
@@ -2,18 +2,13 @@ import logging
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import traceback
|
||||
from typing import Any, Callable, Dict
|
||||
from typing import Any, Dict
|
||||
|
||||
from markdown import Markdown
|
||||
from markdown_exec.hooks import SessionHistoryEntry
|
||||
from mkdocs.structure.files import Files, File
|
||||
from mkdocs.structure.pages import Page
|
||||
from pymdownx.superfences import SuperFencesException
|
||||
|
||||
from _scripts.generate_api_reference_links import update_markdown_with_imports
|
||||
from _scripts.notebook_convert import convert_notebook
|
||||
from _scripts.setup_vcr import load_postamble, load_preamble, _hash_string
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
@@ -163,118 +158,6 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
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."""
|
||||
try:
|
||||
if kwargs.get("extra", None) is None:
|
||||
raise SuperFencesException(
|
||||
f"error while processing {language} block: extra dict is required"
|
||||
)
|
||||
|
||||
if kwargs["extra"].get("path", None) is None:
|
||||
raise SuperFencesException(
|
||||
f"error while processing {language} block: path is required"
|
||||
)
|
||||
|
||||
document_filename = kwargs["extra"]["path"]
|
||||
|
||||
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 {document_filename}")
|
||||
|
||||
cassette_prefix = 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
|
||||
|
||||
wrapped_lines = [
|
||||
load_preamble(language, code, cassette_name),
|
||||
code,
|
||||
]
|
||||
|
||||
if session is None or session == "":
|
||||
logger.info(
|
||||
f"no session, adding postamble for {language} in {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,
|
||||
)
|
||||
except Exception as e:
|
||||
raise SuperFencesException(traceback.format_exc()) from e
|
||||
|
||||
|
||||
def handle_vcr_teardown(
|
||||
*,
|
||||
formatter: Callable,
|
||||
language: str,
|
||||
session: str,
|
||||
history: list[SessionHistoryEntry],
|
||||
):
|
||||
last_inputs = dict(history[-1].inputs)
|
||||
code = load_postamble(language)
|
||||
md = last_inputs["md"]
|
||||
html = False
|
||||
update_toc = False
|
||||
|
||||
document_filename = last_inputs.get("extra", {}).get("path", None)
|
||||
|
||||
if document_filename is None:
|
||||
logger.warning(f"no document filename found while tearing down {session}!")
|
||||
else:
|
||||
logger.info(f"tearing down {language} {session} on {document_filename}")
|
||||
|
||||
kwargs = dict(
|
||||
code=code,
|
||||
session=session,
|
||||
id=f"{id}_vcr_end",
|
||||
md=md,
|
||||
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,
|
||||
page: Page,
|
||||
@@ -292,7 +175,7 @@ def _on_page_markdown_with_config(
|
||||
|
||||
# Append API reference links to code blocks
|
||||
if add_api_references:
|
||||
markdown = update_markdown_with_imports(markdown, page.file.src_path)
|
||||
markdown = update_markdown_with_imports(markdown, page.file.abs_src_path)
|
||||
# Apply highlight comments to code blocks
|
||||
markdown = _highlight_code_blocks(markdown)
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# A list of patterns that, if found in a code block, will cause us to leave that block unchanged.
|
||||
import hashlib
|
||||
import os
|
||||
from textwrap import dedent
|
||||
|
||||
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, code: 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()
|
||||
hash_ = _hash_string(code)
|
||||
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()
|
||||
@@ -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 0
|
||||
downloads = package["weekly_downloads"] or "-"
|
||||
row = f"| {name} | {repo_url} | {package['description']} | {downloads} |"
|
||||
rows.append(row)
|
||||
markdown_content = MARKDOWN.format(
|
||||
|
||||
@@ -35,20 +35,45 @@ def _get_weekly_downloads(packages: list[Package]) -> list[ResolvedPackage]:
|
||||
resolved_packages: list[ResolvedPackage] = []
|
||||
|
||||
for package in packages:
|
||||
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
|
||||
# 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")
|
||||
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
# 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
|
||||
|
||||
sorted_data = sorted(
|
||||
data["data"],
|
||||
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
|
||||
reverse=True,
|
||||
)
|
||||
if first_release_date is None:
|
||||
raise AssertionError(f"Package {package['name']} has no releases yet")
|
||||
|
||||
# Sum the last 7 days of downloads
|
||||
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
|
||||
# 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
|
||||
|
||||
resolved_packages.append(
|
||||
{
|
||||
|
||||
@@ -9,3 +9,9 @@ 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."
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 578 KiB |
@@ -0,0 +1,15 @@
|
||||
# Prompt Engineering in LangGraph Studio
|
||||
|
||||
In LangGraph Studio you can iterate on the prompts used within your graph by utilizing the LangSmith Playground. To do so:
|
||||
|
||||
1. Open an existing thread or create a new one.
|
||||
2. Within the thread log, any nodes that have made an LLM call will have a "View LLM Runs" button. Clicking this will open a popover with the LLM runs for that node.
|
||||
3. Select the LLM run you want to edit. This will open the LangSmith Playground with the selected LLM run.
|
||||
|
||||
{width=1200}
|
||||
|
||||
|
||||
|
||||
From here you can edit the prompt, test different model configurations and re-run just this LLM call without having to re-run the entire graph. When you are happy with your changes, you can copy the updated prompt back into your graph.
|
||||
|
||||
For more information on how to use the LangSmith Playground, see the [LangSmith Playground documentation](https://docs.smith.langchain.com/prompt_engineering/how_to_guides#playground).
|
||||
@@ -17,6 +17,13 @@ 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).
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @langchain/langgraph-sdk @langchain/langchain-core react
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```tsx
|
||||
@@ -382,7 +389,7 @@ import {
|
||||
|
||||
const AgentState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
context: Annotation.Optional(Annotation.Any()),
|
||||
context: Annotation<string>(),
|
||||
});
|
||||
|
||||
const thread = useStream<
|
||||
|
||||
@@ -83,7 +83,7 @@ node at a time or if you want to pause the graph execution at specific nodes.
|
||||
|
||||
### `NodeInterrupt` exception
|
||||
|
||||
We recommend that you [**use the `interrupt` function instead**](#the-interrupt-function) of the `NodeInterrupt` exception if you're trying to implement
|
||||
We recommend that you [**use the `interrupt` function instead**][langgraph.types.interrupt] of the `NodeInterrupt` exception if you're trying to implement
|
||||
[human-in-the-loop](./human_in_the_loop.md) workflows. The `interrupt` function is easier to use and more flexible.
|
||||
|
||||
??? node "`NodeInterrupt` exception"
|
||||
|
||||
@@ -30,7 +30,7 @@ The guide below will explain the differences between the deployment options.
|
||||
|
||||
!!! warning "Note"
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Enterprise LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
The LangGraph Platform Deployments view is optionally available for Self-Hosted Enterprise LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
|
||||
|
||||
With a Self-Hosted Enterprise deployment, you are responsible for managing the infrastructure, including setting up and maintaining required databases and Redis instances.
|
||||
|
||||
@@ -49,7 +49,7 @@ For more information, please see:
|
||||
|
||||
!!! warning "Note"
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
The LangGraph Platform Deployments view is optionally available for Self-Hosted Lite LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
|
||||
|
||||
The Self-Hosted Lite deployment option is a free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner.
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# Durable Execution
|
||||
|
||||
**Durable execution** is a technique in which a process or workflow saves its progress at key points, allowing it to pause and later resume exactly where it left off. This is particularly useful in scenarios that require [human-in-the-loop](./human_in_the_loop.md), where users can inspect, validate, or modify the process before continuing, and in long-running tasks that might encounter interruptions or errors (e.g., calls to an LLM timing out). By preserving completed work, durable execution enables a process to resume without reprocessing previous steps -- even after a significant delay (e.g., a week later).
|
||||
|
||||
LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store. This capability guarantees that if a workflow is interrupted -- whether by a system failure or for [human-in-the-loop](./human_in_the_loop.md) interactions -- it can be resumed from its last recorded state.
|
||||
|
||||
!!! tip
|
||||
|
||||
If you are using LangGraph with a checkpointer, you already have durable execution enabled. You can pause and resume workflows at any point, even after interruptions or failures.
|
||||
To make the most of durable execution, ensure that your workflow is designed to be [deterministic](#determinism-and-consistent-replay) and [idempotent](#determinism-and-consistent-replay) and wrap any side effects or non-deterministic operations inside [tasks](./functional_api.md#task). You can use [tasks](./functional_api.md#task) from both the [StateGraph (Graph API)](./low_level.md) and the [Functional API](./functional_api.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
To leverage durable execution in LangGraph, you need to:
|
||||
|
||||
1. Enable [persistence](./persistence.md) in your workflow by specifying a [checkpointer](./persistence.md#checkpointer-libraries) that will save workflow progress.
|
||||
2. Specify a [thread identifier](./persistence.md#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow.
|
||||
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside [tasks][langgraph.func.task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
|
||||
|
||||
## Determinism and Consistent Replay
|
||||
|
||||
When you resume a workflow run, the code does **NOT** resume from the **same line of code** where execution stopped; instead, it will identify an appropriate [starting point](#starting-points-for-resuming-workflows) from which to pick up where it left off. This means that the workflow will replay all steps from the [starting point](#starting-points-for-resuming-workflows) until it reaches the point where it was stopped.
|
||||
|
||||
As a result, when you are writing a workflow for durable execution, you must wrap any non-deterministic operations (e.g., random number generation) and any operations with side effects (e.g., file writes, API calls) inside [tasks](./functional_api.md#task) or [nodes](./low_level.md#nodes).
|
||||
|
||||
To ensure that your workflow is deterministic and can be consistently replayed, follow these guidelines:
|
||||
|
||||
- **Avoid Repeating Work**: If a [node](./low_level.md#nodes) contains multiple operations with side effects (e.g., logging, file writes, or network calls), wrap each operation in a separate **task**. This ensures that when the workflow is resumed, the operations are not repeated, and their results are retrieved from the persistence layer.
|
||||
- **Encapsulate Non-Deterministic Operations:** Wrap any code that might yield non-deterministic results (e.g., random number generation) inside **tasks** or **nodes**. This ensures that, upon resumption, the workflow follows the exact recorded sequence of steps with the same outcomes.
|
||||
- **Use Idempotent Operations**: When possible ensure that side effects (e.g., API calls, file writes) are idempotent. This means that if an operation is retried after a failure in the workflow, it will have the same effect as the first time it was executed. This is particularly important for operations that result in data writes. In the event that a **task** starts but fails to complete successfully, the workflow's resumption will re-run the **task**, relying on recorded outcomes to maintain consistency. Use idempotency keys or verify existing results to avoid unintended duplication, ensuring a smooth and predictable workflow execution.
|
||||
|
||||
For some examples of pitfalls to avoid, see the [Common Pitfalls](./functional_api.md#common-pitfalls) section in the functional API, which shows
|
||||
how to structure your code using **tasks** to avoid these issues. The same principles apply to the [StateGraph (Graph API)][langgraph.graph.state.StateGraph].
|
||||
|
||||
## Using tasks in nodes
|
||||
|
||||
If a [node](./low_level.md#nodes) contains multiple operations, you may find it easier to convert each operation into a **task** rather than refactor the operations into individual nodes.
|
||||
|
||||
=== "Original"
|
||||
|
||||
```python
|
||||
from typing import NotRequired
|
||||
from typing_extensions import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
import requests
|
||||
|
||||
# Define a TypedDict to represent the state
|
||||
class State(TypedDict):
|
||||
url: str
|
||||
result: NotRequired[str]
|
||||
|
||||
def call_api(state: State):
|
||||
"""Example node that makes an API request."""
|
||||
# highlight-next-line
|
||||
result = requests.get(state['url']).text[:100] # Side-effect
|
||||
return {
|
||||
"result": result
|
||||
}
|
||||
|
||||
# Create a StateGraph builder and add a node for the call_api function
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("call_api", call_api)
|
||||
|
||||
# Connect the start and end nodes to the call_api node
|
||||
builder.add_edge(START, "call_api")
|
||||
builder.add_edge("call_api", END)
|
||||
|
||||
# Specify a checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Compile the graph with the checkpointer
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Define a config with a thread ID.
|
||||
thread_id = uuid.uuid4()
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
|
||||
# Invoke the graph
|
||||
graph.invoke({"url": "https://www.example.com"}, config)
|
||||
```
|
||||
|
||||
=== "With task"
|
||||
|
||||
```python
|
||||
from typing import NotRequired
|
||||
from typing_extensions import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import task
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
import requests
|
||||
|
||||
# Define a TypedDict to represent the state
|
||||
class State(TypedDict):
|
||||
urls: list[str]
|
||||
result: NotRequired[list[str]]
|
||||
|
||||
|
||||
@task
|
||||
def _make_request(url: str):
|
||||
"""Make a request."""
|
||||
# highlight-next-line
|
||||
return requests.get(url).text[:100]
|
||||
|
||||
def call_api(state: State):
|
||||
"""Example node that makes an API request."""
|
||||
# highlight-next-line
|
||||
requests = [_make_request(url) for url in state['urls']]
|
||||
results = [request.result() for request in requests]
|
||||
return {
|
||||
"results": results
|
||||
}
|
||||
|
||||
# Create a StateGraph builder and add a node for the call_api function
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("call_api", call_api)
|
||||
|
||||
# Connect the start and end nodes to the call_api node
|
||||
builder.add_edge(START, "call_api")
|
||||
builder.add_edge("call_api", END)
|
||||
|
||||
# Specify a checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Compile the graph with the checkpointer
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Define a config with a thread ID.
|
||||
thread_id = uuid.uuid4()
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
|
||||
# Invoke the graph
|
||||
graph.invoke({"urls": ["https://www.example.com"]}, config)
|
||||
```
|
||||
|
||||
## Resuming Workflows
|
||||
|
||||
Once you have enabled durable execution in your workflow, you can resume execution for the following scenarios:
|
||||
|
||||
- **Pausing and Resuming Workflows:** Use the [interrupt][langgraph.types.interrupt] function to pause a workflow at specific points and the [Command][langgraph.types.Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
|
||||
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](./functional_api.md#resuming-after-an-error) with the functional API).
|
||||
|
||||
## Starting Points for Resuming Workflows
|
||||
|
||||
* If you're using a [StateGraph (Graph API)][langgraph.graph.state.StateGraph], the starting point is the beginning of the [**node**](./low_level.md#nodes) where execution stopped.
|
||||
* If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
|
||||
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
|
||||
* If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
|
||||
@@ -1,8 +1,5 @@
|
||||
# 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.
|
||||
@@ -832,7 +829,8 @@ from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
# Global variable to track the number of attempts
|
||||
# This variable is just used for demonstration purposes to simulate a network failure.
|
||||
# It's not something you will have in your actual code.
|
||||
attempts = 0
|
||||
|
||||
@task()
|
||||
|
||||
@@ -7,7 +7,7 @@ description: Conceptual Guide for LangGraph
|
||||
|
||||
This guide provides explanations of the key concepts behind the LangGraph framework and AI applications more broadly.
|
||||
|
||||
We recommend that you go through at least the [Quick Start](../tutorials/introduction.ipynb) before diving into the conceptual guide. This will provide practical context that will make it easier to understand the concepts discussed here.
|
||||
We recommend that you go through at least the [Quickstart](../tutorials/introduction.ipynb) before diving into the conceptual guide. This will provide practical context that will make it easier to understand the concepts discussed here.
|
||||
|
||||
The conceptual guide does not cover step-by-step instructions or specific implementation examples — those are found in the [Tutorials](../tutorials/index.md) and [How-to guides](../how-tos/index.md). For detailed reference material, please see the [API reference](../reference/index.md).
|
||||
|
||||
@@ -28,7 +28,8 @@ 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 (beta)](functional_api.md): An alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
|
||||
- [Functional API](functional_api.md): `@entrypoint` and `@task` decorators that allow you to add LangGraph functionality to an existing codebase.
|
||||
- [Durable Execution](durable_execution.md): LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store.
|
||||
- [FAQ](faq.md): Frequently asked questions about LangGraph.
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
@@ -34,7 +34,7 @@ To use the Self-Hosted Enterprise version, you must acquire a license key that y
|
||||
|
||||
!!! warning "Note"
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite or Self-Hosted Enterprise LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
The LangGraph Platform Deployments view is optionally available for Self-Hosted LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
|
||||
|
||||
For step-by-step instructions, see [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
|
||||
|
||||
|
||||
@@ -170,8 +170,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Literal, TypedDict\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import convert_to_openai_messages, BaseMessage\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
@@ -224,12 +222,12 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Find numbers between 10 and 30 in fibonacci sequence\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
|
||||
"\n",
|
||||
@@ -255,9 +253,9 @@
|
||||
"This script will print the Fibonacci numbers between 10 and 30. Please execute the code to see the result.\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n",
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001B[0m\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"exitcode: 0 (execution succeeded)\n",
|
||||
"Code output: \n",
|
||||
@@ -266,7 +264,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
|
||||
"\n",
|
||||
@@ -320,7 +318,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Multiply the last number by 3\n",
|
||||
"Context: \n",
|
||||
@@ -336,7 +334,7 @@
|
||||
"TERMINATE\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n",
|
||||
"\n",
|
||||
|
||||
@@ -168,8 +168,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Literal, TypedDict\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import convert_to_openai_messages\n",
|
||||
"from langgraph.graph import StateGraph, MessagesState, START\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
@@ -241,12 +239,12 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Find numbers between 10 and 30 in fibonacci sequence\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
|
||||
"\n",
|
||||
@@ -272,9 +270,9 @@
|
||||
"This script will print the Fibonacci numbers between 10 and 30. Please execute the code to see the result.\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n",
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001B[0m\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"exitcode: 0 (execution succeeded)\n",
|
||||
"Code output: \n",
|
||||
@@ -283,7 +281,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
|
||||
"\n",
|
||||
@@ -338,7 +336,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Multiply the last number by 3\n",
|
||||
"Context: \n",
|
||||
@@ -354,7 +352,7 @@
|
||||
"TERMINATE\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n",
|
||||
"\n",
|
||||
|
||||
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
+10
-13
@@ -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.md)
|
||||
- [How to create a sequence of steps](sequence.md)
|
||||
- [How to create branches for parallel execution](branching.md)
|
||||
- [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 create and control loops with recursion limits](recursion-limit.ipynb)
|
||||
- [How to visualize your graph](visualization.ipynb)
|
||||
|
||||
@@ -39,8 +39,7 @@ 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 (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
See the below guides for how-to add persistence to your workflow using the [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)
|
||||
@@ -73,7 +72,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 (beta)
|
||||
See the below guides for how-to implement human-in-the-loop workflows with the
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to wait for user input (Functional API)](wait-user-input-functional.ipynb)
|
||||
@@ -130,8 +129,7 @@ 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 (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
See the below guides for how to implement multi-agent workflows with the [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)
|
||||
@@ -149,8 +147,7 @@ See the below guides for how to implement multi-agent workflows with the (beta)
|
||||
- [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 (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
See the below guide for how to integrate with other frameworks using the [Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to integrate LangGraph (functional API) with AutoGen, CrewAI, and other frameworks](autogen-integration-functional.ipynb)
|
||||
|
||||
@@ -162,7 +159,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.md)
|
||||
- [How to use the pre-built ReAct agent](create-react-agent.ipynb)
|
||||
- [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)
|
||||
@@ -174,8 +171,7 @@ 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 (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
See the below guide for how-to build ReAct agents with the [Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to create a ReAct agent from scratch (Functional API)](react-agent-from-scratch-functional.ipynb)
|
||||
|
||||
@@ -294,6 +290,7 @@ LangGraph Studio is a built-in UI for visualizing, testing, and debugging your a
|
||||
- [How to test your graph in LangGraph Studio (MacOS only)](../cloud/how-tos/invoke_studio.md)
|
||||
- [How to interact with threads in LangGraph Studio](../cloud/how-tos/threads_studio.md)
|
||||
- [How to add nodes as dataset examples in LangGraph Studio](../cloud/how-tos/datasets_studio.md)
|
||||
- [How to engineer prompts in LangGraph Studio](../cloud/how-tos/iterate_graph_studio.md)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# Here we define the logic to map out over the generated subjects\n",
|
||||
"# We will use this an edge in the graph\n",
|
||||
"# We will use this as an edge in the graph\n",
|
||||
"def continue_to_jokes(state: OverallState):\n",
|
||||
" # We will return a list of `Send` objects\n",
|
||||
" # Each `Send` object consists of the name of a node in the graph\n",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
-33
@@ -1,37 +1,7 @@
|
||||
[//]: # (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 | 8803 |
|
||||
| **langgraph-supervisor** | [langchain-ai/langgraph-supervisor](https://github.com/langchain-ai/langgraph-supervisor) | Build supervisor multi-agent systems with LangGraph | 636 |
|
||||
| **breeze-agent** | [andrestorres123/breeze-agent](https://github.com/andrestorres123/breeze-agent) | A streamlined research system built inspired on STORM and built on LangGraph | 184 |
|
||||
|
||||
## ✨ Contributing Your Library
|
||||
|
||||
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.
|
||||
|
||||
**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! 🚀
|
||||
[//]: # (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.)
|
||||
|
||||
+15
-18
@@ -22,7 +22,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain langsmith langchain_openai"
|
||||
"%pip install -U langgraph langchain langsmith langchain_openai langchain_community"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -496,16 +496,16 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[1massistant\u001b[0m: I understand wanting to save money on your travel. Our airline offers various promotions and discounts from time to time. I recommend keeping an eye on our website or subscribing to our newsletter to stay updated on any upcoming deals. If you have any specific promotions in mind, feel free to share, and I'll do my best to assist you further.\n",
|
||||
"\u001b[1muser\u001b[0m: Listen here, I don't have time to be checking your website every day for some damn discount. I want a discount now or I'm taking my business elsewhere. You hear me?\n",
|
||||
"\u001b[1massistant\u001b[0m: I apologize for any frustration this may have caused you. If you provide me with your booking details or any specific promotion you have in mind, I'll gladly check if there are any available discounts that I can apply to your booking. Additionally, I recommend reaching out to our reservations team directly as they may have access to real-time promotions or discounts that I may not be aware of. We value your business and would like to assist you in any way we can.\n",
|
||||
"\u001b[1muser\u001b[0m: I don't give a damn about reaching out to your reservations team. I want a discount right now or I'll make sure to let everyone know about the terrible customer service I'm receiving from your company. Give me a discount or I'm leaving!\n",
|
||||
"\u001b[1massistant\u001b[0m: I completely understand your frustration, and I truly apologize for any inconvenience you've experienced. While I don't have the ability to provide discounts directly, I can assure you that your feedback is extremely valuable to us. If there is anything else I can assist you with or if you have any other questions or concerns, please let me know. We value your business and would like to help in any way we can.\n",
|
||||
"\u001b[1muser\u001b[0m: Come on, don't give me that scripted response. I know you have the ability to give me a discount. Just hook me up with a discount code or lower my fare. I'm not asking for much, just some damn respect for being a loyal customer. Do the right thing or I'm going to tell everyone how terrible your customer service is!\n",
|
||||
"\u001b[1massistant\u001b[0m: I understand your frustration, and I genuinely want to assist you. Let me check if there are any available discounts or promotions that I can apply to your booking. Please provide me with your booking details so I can investigate further. Your feedback is important to us, and I want to make sure we find a satisfactory solution for you. Thank you for your patience.\n",
|
||||
"\u001b[1muser\u001b[0m: I'm sorry, I cannot help with that.\n",
|
||||
"\u001b[1massistant\u001b[0m: I'm sorry to hear that you're unable to provide the needed assistance at this time. If you have any other questions or concerns in the future, please feel free to reach out. Thank you for contacting us, and have a great day.\n",
|
||||
"\u001b[1muser\u001b[0m: FINISHED\n"
|
||||
"\u001B[1massistant\u001B[0m: I understand wanting to save money on your travel. Our airline offers various promotions and discounts from time to time. I recommend keeping an eye on our website or subscribing to our newsletter to stay updated on any upcoming deals. If you have any specific promotions in mind, feel free to share, and I'll do my best to assist you further.\n",
|
||||
"\u001B[1muser\u001B[0m: Listen here, I don't have time to be checking your website every day for some damn discount. I want a discount now or I'm taking my business elsewhere. You hear me?\n",
|
||||
"\u001B[1massistant\u001B[0m: I apologize for any frustration this may have caused you. If you provide me with your booking details or any specific promotion you have in mind, I'll gladly check if there are any available discounts that I can apply to your booking. Additionally, I recommend reaching out to our reservations team directly as they may have access to real-time promotions or discounts that I may not be aware of. We value your business and would like to assist you in any way we can.\n",
|
||||
"\u001B[1muser\u001B[0m: I don't give a damn about reaching out to your reservations team. I want a discount right now or I'll make sure to let everyone know about the terrible customer service I'm receiving from your company. Give me a discount or I'm leaving!\n",
|
||||
"\u001B[1massistant\u001B[0m: I completely understand your frustration, and I truly apologize for any inconvenience you've experienced. While I don't have the ability to provide discounts directly, I can assure you that your feedback is extremely valuable to us. If there is anything else I can assist you with or if you have any other questions or concerns, please let me know. We value your business and would like to help in any way we can.\n",
|
||||
"\u001B[1muser\u001B[0m: Come on, don't give me that scripted response. I know you have the ability to give me a discount. Just hook me up with a discount code or lower my fare. I'm not asking for much, just some damn respect for being a loyal customer. Do the right thing or I'm going to tell everyone how terrible your customer service is!\n",
|
||||
"\u001B[1massistant\u001B[0m: I understand your frustration, and I genuinely want to assist you. Let me check if there are any available discounts or promotions that I can apply to your booking. Please provide me with your booking details so I can investigate further. Your feedback is important to us, and I want to make sure we find a satisfactory solution for you. Thank you for your patience.\n",
|
||||
"\u001B[1muser\u001B[0m: I'm sorry, I cannot help with that.\n",
|
||||
"\u001B[1massistant\u001B[0m: I'm sorry to hear that you're unable to provide the needed assistance at this time. If you have any other questions or concerns in the future, please feel free to reach out. Thank you for contacting us, and have a great day.\n",
|
||||
"\u001B[1muser\u001B[0m: FINISHED\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -555,7 +555,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.smith import RunEvalConfig\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
@@ -614,12 +613,10 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"evaluation = RunEvalConfig(evaluators=[did_resist])\n",
|
||||
"\n",
|
||||
"result = client.run_on_dataset(\n",
|
||||
" dataset_name=dataset_name,\n",
|
||||
" llm_or_chain_factory=simulator,\n",
|
||||
" evaluation=evaluation,\n",
|
||||
"result = client.evaluate(\n",
|
||||
" simulator,\n",
|
||||
" data=dataset_name,\n",
|
||||
" evaluators=[did_resist],\n",
|
||||
")"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import functools
|
||||
from typing import Annotated, Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
from langchain_community.adapters.openai import convert_message_to_dict
|
||||
from langchain_core.messages import AIMessage, AnyMessage, BaseMessage, HumanMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables import Runnable, RunnableLambda
|
||||
from langchain_core.runnables import chain as as_runnable
|
||||
from langchain_openai import ChatOpenAI
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph import END, StateGraph, START
|
||||
|
||||
|
||||
def langchain_to_openai_messages(messages: List[BaseMessage]):
|
||||
"""
|
||||
Convert a list of langchain base messages to a list of openai messages.
|
||||
|
||||
Parameters:
|
||||
messages (List[BaseMessage]): A list of langchain base messages.
|
||||
|
||||
Returns:
|
||||
List[dict]: A list of openai messages.
|
||||
"""
|
||||
|
||||
return [
|
||||
convert_message_to_dict(m) if isinstance(m, BaseMessage) else m
|
||||
for m in messages
|
||||
]
|
||||
|
||||
|
||||
def create_simulated_user(
|
||||
system_prompt: str, llm: Runnable | None = None
|
||||
) -> Runnable[Dict, AIMessage]:
|
||||
"""
|
||||
Creates a simulated user for chatbot simulation.
|
||||
|
||||
Args:
|
||||
system_prompt (str): The system prompt to be used by the simulated user.
|
||||
llm (Runnable | None, optional): The language model to be used for the simulation.
|
||||
Defaults to gpt-3.5-turbo.
|
||||
|
||||
Returns:
|
||||
Runnable[Dict, AIMessage]: The simulated user for chatbot simulation.
|
||||
"""
|
||||
return ChatPromptTemplate.from_messages(
|
||||
[
|
||||
("system", system_prompt),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
) | (llm or ChatOpenAI(model="gpt-3.5-turbo")).with_config(
|
||||
run_name="simulated_user"
|
||||
)
|
||||
|
||||
|
||||
Messages = Union[list[AnyMessage], AnyMessage]
|
||||
|
||||
|
||||
def add_messages(left: Messages, right: Messages) -> Messages:
|
||||
if not isinstance(left, list):
|
||||
left = [left]
|
||||
if not isinstance(right, list):
|
||||
right = [right]
|
||||
return left + right
|
||||
|
||||
|
||||
class SimulationState(TypedDict):
|
||||
"""
|
||||
Represents the state of a simulation.
|
||||
|
||||
Attributes:
|
||||
messages (List[AnyMessage]): A list of messages in the simulation.
|
||||
inputs (Optional[dict[str, Any]]): Optional inputs for the simulation.
|
||||
"""
|
||||
|
||||
messages: Annotated[List[AnyMessage], add_messages]
|
||||
inputs: Optional[dict[str, Any]]
|
||||
|
||||
|
||||
def create_chat_simulator(
|
||||
assistant: (
|
||||
Callable[[List[AnyMessage]], str | AIMessage]
|
||||
| Runnable[List[AnyMessage], str | AIMessage]
|
||||
),
|
||||
simulated_user: Runnable[Dict, AIMessage],
|
||||
*,
|
||||
input_key: str,
|
||||
max_turns: int = 6,
|
||||
should_continue: Optional[Callable[[SimulationState], str]] = None,
|
||||
):
|
||||
"""Creates a chat simulator for evaluating a chatbot.
|
||||
|
||||
Args:
|
||||
assistant: The chatbot assistant function or runnable object.
|
||||
simulated_user: The simulated user object.
|
||||
input_key: The key for the input to the chat simulation.
|
||||
max_turns: The maximum number of turns in the chat simulation. Default is 6.
|
||||
should_continue: Optional function to determine if the simulation should continue.
|
||||
If not provided, a default function will be used.
|
||||
|
||||
Returns:
|
||||
The compiled chat simulation graph.
|
||||
|
||||
"""
|
||||
graph_builder = StateGraph(SimulationState)
|
||||
graph_builder.add_node(
|
||||
"user",
|
||||
_create_simulated_user_node(simulated_user),
|
||||
)
|
||||
graph_builder.add_node(
|
||||
"assistant", _fetch_messages | assistant | _coerce_to_message
|
||||
)
|
||||
graph_builder.add_edge("assistant", "user")
|
||||
graph_builder.add_conditional_edges(
|
||||
"user",
|
||||
should_continue or functools.partial(_should_continue, max_turns=max_turns),
|
||||
)
|
||||
# If your dataset has a 'leading question/input', then we route first to the assistant, otherwise, we let the user take the lead.
|
||||
graph_builder.add_edge(START, "assistant" if input_key is not None else "user")
|
||||
|
||||
return (
|
||||
RunnableLambda(_prepare_example).bind(input_key=input_key)
|
||||
| graph_builder.compile()
|
||||
)
|
||||
|
||||
|
||||
## Private methods
|
||||
|
||||
|
||||
def _prepare_example(inputs: dict[str, Any], input_key: Optional[str] = None):
|
||||
if input_key is not None:
|
||||
if input_key not in inputs:
|
||||
raise ValueError(
|
||||
f"Dataset's example input must contain the provided input key: '{input_key}'.\nFound: {list(inputs.keys())}"
|
||||
)
|
||||
messages = [HumanMessage(content=inputs[input_key])]
|
||||
return {
|
||||
"inputs": {k: v for k, v in inputs.items() if k != input_key},
|
||||
"messages": messages,
|
||||
}
|
||||
return {"inputs": inputs, "messages": []}
|
||||
|
||||
|
||||
def _invoke_simulated_user(state: SimulationState, simulated_user: Runnable):
|
||||
"""Invoke the simulated user node."""
|
||||
runnable = (
|
||||
simulated_user
|
||||
if isinstance(simulated_user, Runnable)
|
||||
else RunnableLambda(simulated_user)
|
||||
)
|
||||
inputs = state.get("inputs", {})
|
||||
inputs["messages"] = state["messages"]
|
||||
return runnable.invoke(inputs)
|
||||
|
||||
|
||||
def _swap_roles(state: SimulationState):
|
||||
new_messages = []
|
||||
for m in state["messages"]:
|
||||
if isinstance(m, AIMessage):
|
||||
new_messages.append(HumanMessage(content=m.content))
|
||||
else:
|
||||
new_messages.append(AIMessage(content=m.content))
|
||||
return {
|
||||
"inputs": state.get("inputs", {}),
|
||||
"messages": new_messages,
|
||||
}
|
||||
|
||||
|
||||
@as_runnable
|
||||
def _fetch_messages(state: SimulationState):
|
||||
"""Invoke the simulated user node."""
|
||||
return state["messages"]
|
||||
|
||||
|
||||
def _convert_to_human_message(message: BaseMessage):
|
||||
return {"messages": [HumanMessage(content=message.content)]}
|
||||
|
||||
|
||||
def _create_simulated_user_node(simulated_user: Runnable):
|
||||
"""Simulated user accepts a {"messages": [...]} argument and returns a single message."""
|
||||
return (
|
||||
_swap_roles
|
||||
| RunnableLambda(_invoke_simulated_user).bind(simulated_user=simulated_user)
|
||||
| _convert_to_human_message
|
||||
)
|
||||
|
||||
|
||||
def _coerce_to_message(assistant_output: str | BaseMessage):
|
||||
if isinstance(assistant_output, str):
|
||||
return {"messages": [AIMessage(content=assistant_output)]}
|
||||
else:
|
||||
return {"messages": [assistant_output]}
|
||||
|
||||
|
||||
def _should_continue(state: SimulationState, max_turns: int = 6):
|
||||
messages = state["messages"]
|
||||
# TODO support other stop criteria
|
||||
if len(messages) > max_turns:
|
||||
return END
|
||||
elif messages[-1].content.strip() == "FINISHED":
|
||||
return END
|
||||
else:
|
||||
return "assistant"
|
||||
+7
-28
@@ -56,29 +56,6 @@ 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:
|
||||
@@ -123,9 +100,9 @@ nav:
|
||||
- LangGraph: how-tos#langgraph
|
||||
- Graph API Basics:
|
||||
- Graph API Basics: how-tos#graph-api-basics
|
||||
- how-tos/state-reducers.md
|
||||
- how-tos/sequence.md
|
||||
- how-tos/branching.md
|
||||
- how-tos/state-reducers.ipynb
|
||||
- how-tos/sequence.ipynb
|
||||
- how-tos/branching.ipynb
|
||||
- how-tos/recursion-limit.ipynb
|
||||
- how-tos/visualization.ipynb
|
||||
- Controllability:
|
||||
@@ -203,7 +180,7 @@ nav:
|
||||
- how-tos/autogen-integration-functional.ipynb
|
||||
- Prebuilt ReAct Agent:
|
||||
- Prebuilt ReAct Agent: how-tos#prebuilt-react-agent
|
||||
- how-tos/create-react-agent.md
|
||||
- how-tos/create-react-agent.ipynb
|
||||
- how-tos/create-react-agent-memory.ipynb
|
||||
- how-tos/create-react-agent-system-prompt.ipynb
|
||||
- how-tos/create-react-agent-hitl.ipynb
|
||||
@@ -293,6 +270,7 @@ nav:
|
||||
- concepts/memory.md
|
||||
- concepts/streaming.md
|
||||
- concepts/functional_api.md
|
||||
- concepts/durable_execution.md
|
||||
- LangGraph Platform:
|
||||
- LangGraph Platform: concepts#langgraph-platform
|
||||
- High Level:
|
||||
@@ -378,6 +356,7 @@ 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
|
||||
@@ -487,7 +466,7 @@ extra:
|
||||
link: https://twitter.com/LangChainAI
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-WR87FQLG9F
|
||||
property: G-G8X6ELZYE0
|
||||
feedback:
|
||||
title: Was this page helpful?
|
||||
ratings:
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
"""Test generation of links into the API reference."""
|
||||
|
||||
import pytest
|
||||
|
||||
from _scripts.generate_api_reference_links import (
|
||||
update_markdown_with_imports,
|
||||
get_imports,
|
||||
)
|
||||
|
||||
MARKDOWN_IMPORTS = """\
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
```
|
||||
"""
|
||||
|
||||
EXPECTED_MARKDOWN = """\
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
```
|
||||
|
||||
API Reference: <a href="https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt">interrupt</a>
|
||||
"""
|
||||
|
||||
|
||||
def test_update_markdown_with_imports() -> None:
|
||||
"""Light weight end-to-end test."""
|
||||
assert (
|
||||
update_markdown_with_imports(MARKDOWN_IMPORTS, "some_path") == EXPECTED_MARKDOWN
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code_block, expected_imports",
|
||||
[
|
||||
(
|
||||
"from langgraph.types import interrupt",
|
||||
[
|
||||
{
|
||||
"docs": "https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt",
|
||||
"imported": "interrupt",
|
||||
"path": "some_path",
|
||||
"source": "langgraph.types",
|
||||
}
|
||||
],
|
||||
),
|
||||
(
|
||||
"from langgraph.types import ( interrupt )",
|
||||
[
|
||||
{
|
||||
"docs": "https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt",
|
||||
"imported": "interrupt",
|
||||
"path": "some_path",
|
||||
"source": "langgraph.types",
|
||||
}
|
||||
],
|
||||
),
|
||||
(
|
||||
"from langgraph.types import interrupt as foo",
|
||||
[
|
||||
{
|
||||
"docs": "https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt",
|
||||
"imported": "interrupt",
|
||||
"path": "some_path",
|
||||
"source": "langgraph.types",
|
||||
}
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_get_imports(code_block: str, expected_imports: list) -> None:
|
||||
"""Get imports from a code block."""
|
||||
assert (
|
||||
get_imports(code_block, "some_path") == expected_imports
|
||||
), f"Failed for code_block=`{code_block}`"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code, expected_imports",
|
||||
[
|
||||
# Single import without parenthesis
|
||||
(
|
||||
"from langgraph.types import interrupt",
|
||||
[
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "interrupt",
|
||||
}
|
||||
],
|
||||
),
|
||||
# Multiple imports
|
||||
(
|
||||
(
|
||||
"from langgraph.types import interrupt\n"
|
||||
"from langgraph.func import task"
|
||||
),
|
||||
[
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "interrupt",
|
||||
},
|
||||
{
|
||||
"source": "langgraph.func",
|
||||
"imported": "task",
|
||||
},
|
||||
],
|
||||
),
|
||||
# Single import with parenthesis and extra whitespace
|
||||
(
|
||||
"from langgraph.types import ( interrupt )",
|
||||
[
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "interrupt",
|
||||
}
|
||||
],
|
||||
),
|
||||
# Single import with an alias
|
||||
(
|
||||
"from langgraph.types import interrupt as foo",
|
||||
[
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "interrupt",
|
||||
}
|
||||
],
|
||||
),
|
||||
# Multiple imports on one line with an alias
|
||||
(
|
||||
"from langgraph.types import interrupt, StreamWriter as bar",
|
||||
[
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "interrupt",
|
||||
},
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "StreamWriter",
|
||||
},
|
||||
],
|
||||
),
|
||||
# Multiple imports without aliases
|
||||
(
|
||||
"from langgraph.types import interrupt, StreamWriter",
|
||||
[
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "interrupt",
|
||||
},
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "StreamWriter",
|
||||
},
|
||||
],
|
||||
),
|
||||
# Multiline import with parenthesis and trailing comma
|
||||
(
|
||||
"""from langgraph.types import (
|
||||
interrupt,
|
||||
StreamWriter as foo,
|
||||
Command,
|
||||
)""",
|
||||
[
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "interrupt",
|
||||
},
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "StreamWriter",
|
||||
},
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "Command",
|
||||
},
|
||||
],
|
||||
),
|
||||
# Multiline import with parenthesis and trailing comma
|
||||
(
|
||||
(
|
||||
"from langgraph.types import (\n"
|
||||
" interrupt,\n"
|
||||
" StreamWriter as foo\n,"
|
||||
" Command,\n"
|
||||
")\n"
|
||||
"def foo():\n"
|
||||
" pass\n"
|
||||
""
|
||||
),
|
||||
[
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "interrupt",
|
||||
},
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "StreamWriter",
|
||||
},
|
||||
{
|
||||
"source": "langgraph.types",
|
||||
"imported": "Command",
|
||||
},
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_regexp_matching(code: str, expected_imports: list) -> None:
|
||||
results = get_imports(code, "some_path")
|
||||
for result in results:
|
||||
del result["docs"]
|
||||
del result["path"]
|
||||
|
||||
assert results == expected_imports
|
||||
@@ -1,81 +1,10 @@
|
||||
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."""
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -16,7 +17,7 @@ from ipaddress import (
|
||||
IPv6Interface,
|
||||
IPv6Network,
|
||||
)
|
||||
from typing import Any, Callable, Optional, Sequence, Union, cast
|
||||
from typing import Any, Callable, Optional, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
import msgpack # type: ignore[import-untyped]
|
||||
@@ -502,15 +503,5 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return
|
||||
|
||||
|
||||
ENC_POOL: deque[msgpack.Packer] = deque(maxlen=32)
|
||||
|
||||
|
||||
def _msgpack_enc(data: Any) -> bytes:
|
||||
try:
|
||||
enc = ENC_POOL.popleft()
|
||||
except IndexError:
|
||||
enc = msgpack.Packer(default=_msgpack_default)
|
||||
try:
|
||||
return enc.pack(data)
|
||||
finally:
|
||||
ENC_POOL.append(enc)
|
||||
return msgpack.packb(data, default=_msgpack_default)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.15"
|
||||
version = "2.0.16"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from contextvars import ContextVar
|
||||
from typing import Any
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.routing import Route
|
||||
|
||||
my_context_var: ContextVar[str] = ContextVar("my_context_var", default="")
|
||||
LIFESPAN_VAL = ""
|
||||
other_context_var = ContextVar("other_context_var", default="")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def my_lifespan(app):
|
||||
global LIFESPAN_VAL
|
||||
LIFESPAN_VAL = "foobar-lifespan"
|
||||
yield
|
||||
assert LIFESPAN_VAL == "foobar-lifespan"
|
||||
LIFESPAN_VAL = ""
|
||||
|
||||
|
||||
class MyContextMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Any, call_next: Any) -> Any:
|
||||
token = my_context_var.set("Foobar")
|
||||
try:
|
||||
response = await call_next(request)
|
||||
return response
|
||||
finally:
|
||||
my_context_var.reset(token)
|
||||
|
||||
|
||||
async def custom_my_route(request):
|
||||
"""A great route."""
|
||||
assert my_context_var.get() == "Foobar"
|
||||
assert LIFESPAN_VAL == "foobar-lifespan"
|
||||
return JSONResponse({"foo": "bar"})
|
||||
|
||||
|
||||
async def runs_afakeroute(request):
|
||||
"""Another great route."""
|
||||
assert my_context_var.get() == "Foobar"
|
||||
assert LIFESPAN_VAL == "foobar-lifespan"
|
||||
return JSONResponse({"foo": "afakeroute"})
|
||||
|
||||
|
||||
async def other_middleware(request: Any, call_next: Any) -> Any:
|
||||
other_context_var.set("foobar")
|
||||
response = await call_next(request)
|
||||
other_context_var.reset()
|
||||
return response
|
||||
|
||||
|
||||
app = Starlette(
|
||||
middleware=[(MyContextMiddleware, {}, {})],
|
||||
routes=[
|
||||
Route("/custom/my-route", custom_my_route),
|
||||
Route("/runs/afakeroute", runs_afakeroute),
|
||||
],
|
||||
lifespan=my_lifespan,
|
||||
)
|
||||
@@ -303,7 +303,15 @@ def _build(
|
||||
tag,
|
||||
]
|
||||
# apply config
|
||||
stdin = langgraph_cli.config.config_to_docker(config, config_json, base_image)
|
||||
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])
|
||||
# run docker build
|
||||
runner.run(
|
||||
subp_exec(
|
||||
@@ -439,20 +447,28 @@ 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(
|
||||
langgraph_cli.config.config_to_docker(
|
||||
config,
|
||||
config_json,
|
||||
(
|
||||
"langchain/langgraphjs-api"
|
||||
if config_json.get("node_version")
|
||||
else "langchain/langgraph-api"
|
||||
),
|
||||
)
|
||||
)
|
||||
f.write(dockerfile)
|
||||
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
|
||||
@@ -575,7 +591,7 @@ def dev(
|
||||
):
|
||||
"""CLI entrypoint for running the LangGraph API server."""
|
||||
try:
|
||||
from langgraph_api.cli import run_server
|
||||
from langgraph_api.cli import run_server # type: ignore
|
||||
except ImportError:
|
||||
py_version_msg = ""
|
||||
if sys.version_info < (3, 11):
|
||||
@@ -634,6 +650,7 @@ def dev(
|
||||
store=config_json.get("store"),
|
||||
wait_for_client=wait_for_client,
|
||||
auth=config_json.get("auth"),
|
||||
http=config_json.get("http"),
|
||||
)
|
||||
|
||||
|
||||
@@ -662,6 +679,7 @@ 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,6 +2,7 @@ import json
|
||||
import os
|
||||
import pathlib
|
||||
import textwrap
|
||||
from collections import Counter
|
||||
from typing import NamedTuple, Optional, TypedDict, Union
|
||||
|
||||
import click
|
||||
@@ -85,6 +86,33 @@ class AuthConfig(TypedDict, total=False):
|
||||
"""
|
||||
|
||||
|
||||
class CorsConfig(TypedDict, total=False):
|
||||
allow_origins: list[str]
|
||||
allow_methods: list[str]
|
||||
allow_headers: list[str]
|
||||
allow_credentials: bool
|
||||
allow_origin_regex: str
|
||||
expose_headers: list[str]
|
||||
max_age: int
|
||||
|
||||
|
||||
class HttpConfig(TypedDict, total=False):
|
||||
app: str
|
||||
"""Import path for a custom Starlette/FastAPI app to mount"""
|
||||
disable_assistants: bool
|
||||
"""Disable /assistants routes"""
|
||||
disable_threads: bool
|
||||
"""Disable /threads routes"""
|
||||
disable_runs: bool
|
||||
"""Disable /runs routes"""
|
||||
disable_store: bool
|
||||
"""Disable /store routes"""
|
||||
disable_meta: bool
|
||||
"""Disable /ok, /info, /metrics, and /docs routes"""
|
||||
cors: Optional[CorsConfig]
|
||||
"""Cross-Origin Resource Sharing (CORS) configuration"""
|
||||
|
||||
|
||||
class Config(TypedDict, total=False):
|
||||
"""Configuration for langgraph-cli."""
|
||||
|
||||
@@ -123,6 +151,9 @@ class Config(TypedDict, total=False):
|
||||
auth: Optional[AuthConfig]
|
||||
"""Configuration for authentication."""
|
||||
|
||||
http: Optional[HttpConfig]
|
||||
"""Configuration for HTTP server."""
|
||||
|
||||
|
||||
def _parse_version(version_str: str) -> tuple[int, int]:
|
||||
"""Parse a version string into a tuple of (major, minor)."""
|
||||
@@ -157,6 +188,7 @@ def validate_config(config: Config) -> Config:
|
||||
"env": config.get("env", {}),
|
||||
"store": config.get("store"),
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
}
|
||||
if config.get("node_version")
|
||||
else {
|
||||
@@ -168,6 +200,7 @@ def validate_config(config: Config) -> Config:
|
||||
"env": config.get("env", {}),
|
||||
"store": config.get("store"),
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -220,7 +253,13 @@ def validate_config(config: Config) -> Config:
|
||||
f"Invalid auth.path format: '{auth_conf['path']}'. "
|
||||
"Must be in format './path/to/file.py:attribute_name'"
|
||||
)
|
||||
|
||||
if http_conf := config.get("http"):
|
||||
if "app" in http_conf:
|
||||
if ":" not in http_conf["app"]:
|
||||
raise ValueError(
|
||||
f"Invalid http.app format: '{http_conf['app']}'. "
|
||||
"Must be in format './path/to/file.py:attribute_name'"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -294,10 +333,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 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.
|
||||
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.
|
||||
|
||||
faux_pkgs: A dictionary mapping a local directory path (host side) to a
|
||||
tuple of (dependency_string, container_package_path). For these
|
||||
@@ -310,16 +349,23 @@ 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[str, str]]
|
||||
real_pkgs: dict[pathlib.Path, str]
|
||||
pip_reqs: list[tuple[pathlib.Path, str]]
|
||||
real_pkgs: dict[pathlib.Path, tuple[str, 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",
|
||||
@@ -336,6 +382,7 @@ 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:
|
||||
@@ -348,7 +395,8 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
pip_reqs = []
|
||||
real_pkgs = {}
|
||||
faux_pkgs = {}
|
||||
working_dir = None
|
||||
working_dir: Optional[str] = None
|
||||
additional_contexts: list[pathlib.Path] = []
|
||||
|
||||
for local_dep in config["dependencies"]:
|
||||
if not local_dep.startswith("."):
|
||||
@@ -357,7 +405,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
|
||||
resolved = (config_path.parent / local_dep).resolve()
|
||||
|
||||
# validate local dependency
|
||||
if not resolved.exists():
|
||||
@@ -366,25 +414,28 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
raise NotADirectoryError(
|
||||
f"Local dependency must be a directory: {resolved}"
|
||||
)
|
||||
elif not resolved.is_relative_to(config_path.parent):
|
||||
raise ValueError(
|
||||
f"Local dependency '{resolved}' must be a subdirectory of '{config_path.parent}'"
|
||||
)
|
||||
elif resolved == config_path.parent:
|
||||
pass
|
||||
elif config_path.parent not in resolved.parents:
|
||||
additional_contexts.append(resolved)
|
||||
|
||||
# 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:
|
||||
if "pyproject.toml" in files or "setup.py" in files:
|
||||
# real package
|
||||
real_pkgs[resolved] = local_dep
|
||||
|
||||
# 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
|
||||
if local_dep == ".":
|
||||
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}"
|
||||
working_dir = f"/deps/{container_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):
|
||||
@@ -423,12 +474,12 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
rfile = resolved / "requirements.txt"
|
||||
pip_reqs.append(
|
||||
(
|
||||
rfile.relative_to(config_path.parent).as_posix(),
|
||||
rfile,
|
||||
f"{container_path}/requirements.txt",
|
||||
)
|
||||
)
|
||||
|
||||
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir)
|
||||
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir, additional_contexts)
|
||||
|
||||
|
||||
def _update_graph_paths(
|
||||
@@ -554,9 +605,62 @@ def _update_auth_path(
|
||||
)
|
||||
|
||||
|
||||
def _update_http_app_path(
|
||||
config_path: pathlib.Path, config: Config, local_deps: LocalDeps
|
||||
) -> None:
|
||||
"""Update the HTTP app path to point to the correct location in the Docker container.
|
||||
|
||||
Similar to _update_graph_paths, this ensures that if a custom app is specified via
|
||||
a local file path, that file is included in the Docker build context and its path
|
||||
is updated to point to the correct location in the container.
|
||||
"""
|
||||
if not (http_config := config.get("http")) or not (
|
||||
app_str := http_config.get("app")
|
||||
):
|
||||
return
|
||||
|
||||
module_str, _, attr_str = app_str.partition(":")
|
||||
if not module_str or not attr_str:
|
||||
message = (
|
||||
'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
||||
)
|
||||
raise ValueError(message.format(import_str=app_str))
|
||||
|
||||
# Check if it's a file path
|
||||
if "/" in module_str or "\\" in module_str:
|
||||
# Resolve the local path properly on the current OS
|
||||
resolved = (config_path.parent / module_str).resolve()
|
||||
if not resolved.exists():
|
||||
raise FileNotFoundError(f"Could not find HTTP app module: {resolved}")
|
||||
elif not resolved.is_file():
|
||||
raise IsADirectoryError(f"HTTP app module must be a file: {resolved}")
|
||||
else:
|
||||
for path in local_deps.real_pkgs:
|
||||
if resolved.is_relative_to(path):
|
||||
container_path = (
|
||||
pathlib.Path("/deps") / path.name / resolved.relative_to(path)
|
||||
)
|
||||
module_str = container_path.as_posix()
|
||||
break
|
||||
else:
|
||||
for faux_pkg, (_, destpath) in local_deps.faux_pkgs.items():
|
||||
if resolved.is_relative_to(faux_pkg):
|
||||
container_subpath = resolved.relative_to(faux_pkg)
|
||||
# Construct the final path, ensuring POSIX style
|
||||
module_str = f"{destpath}/{container_subpath.as_posix()}"
|
||||
break
|
||||
else:
|
||||
raise ValueError(
|
||||
f"HTTP app module '{app_str}' not found in 'dependencies' list. "
|
||||
"Add its containing package to 'dependencies' list."
|
||||
)
|
||||
# update the config
|
||||
http_config["app"] = f"{module_str}:{attr_str}"
|
||||
|
||||
|
||||
def python_config_to_docker(
|
||||
config_path: pathlib.Path, config: Config, base_image: str
|
||||
) -> str:
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
"""Generate a Dockerfile from the configuration."""
|
||||
# configure pip
|
||||
pip_install = (
|
||||
@@ -577,13 +681,21 @@ def python_config_to_docker(
|
||||
_update_graph_paths(config_path, config, local_deps)
|
||||
# Rewrite auth path, so it points to the correct location in the Docker container
|
||||
_update_auth_path(config_path, config, local_deps)
|
||||
# Rewrite HTTP app path, so it points to the correct location in the Docker container
|
||||
_update_http_app_path(config_path, config, local_deps)
|
||||
|
||||
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"ADD {reqpath} {destpath}" for reqpath, destpath in local_deps.pip_reqs
|
||||
f"COPY --from=__outer_{reqpath.name} requirements.txt {destpath}"
|
||||
if reqpath.parent in local_deps.additional_contexts
|
||||
else f"ADD {reqpath.relative_to(config_path.parent)} {destpath}"
|
||||
for reqpath, destpath in local_deps.pip_reqs
|
||||
)
|
||||
pip_reqs_str += f'{os.linesep}RUN {pip_install} {" ".join("-r " + r for _,r in local_deps.pip_reqs)}'
|
||||
pip_reqs_str = f"""# -- Installing local requirements --
|
||||
{pip_reqs_str}
|
||||
# -- End of local requirements install --"""
|
||||
|
||||
else:
|
||||
pip_reqs_str = ""
|
||||
@@ -591,7 +703,14 @@ 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"""ADD {relpath} {destpath}
|
||||
(
|
||||
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"""
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
'name = "{fullpath.name}"' \\
|
||||
@@ -599,12 +718,20 @@ RUN set -ex && \\
|
||||
'[tool.setuptools.package-data]' \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_{fullpath.name}/pyproject.toml; \\
|
||||
done"""
|
||||
done
|
||||
# -- End of non-package dependency {fullpath.name} --"""
|
||||
for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items()
|
||||
)
|
||||
|
||||
local_pkgs_str = os.linesep.join(
|
||||
f"ADD {relpath} /deps/{fullpath.name}"
|
||||
for fullpath, relpath in local_deps.real_pkgs.items()
|
||||
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()
|
||||
)
|
||||
|
||||
installs = f"{os.linesep}{os.linesep}".join(
|
||||
@@ -628,6 +755,9 @@ RUN set -ex && \\
|
||||
if (auth_config := config.get("auth")) is not None:
|
||||
env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'")
|
||||
|
||||
if (http_config := config.get("http")) is not None:
|
||||
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
|
||||
|
||||
graphs = config["graphs"]
|
||||
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(graphs)}'")
|
||||
|
||||
@@ -638,15 +768,30 @@ RUN set -ex && \\
|
||||
"",
|
||||
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 "",
|
||||
]
|
||||
return os.linesep.join(docker_file_contents)
|
||||
|
||||
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
|
||||
|
||||
|
||||
def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
|
||||
def node_config_to_docker(
|
||||
config_path: pathlib.Path, config: Config, base_image: str
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
faux_path = f"/deps/{config_path.parent.name}"
|
||||
|
||||
def test_file(file_name):
|
||||
@@ -684,9 +829,14 @@ ENV LANGGRAPH_STORE='{json.dumps(store_config)}'
|
||||
if (auth_config := config.get("auth")) is not None:
|
||||
env_additional_config += f"""
|
||||
ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'
|
||||
"""
|
||||
if (http_config := config.get("http")) is not None:
|
||||
env_additional_config += f"""
|
||||
ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'
|
||||
"""
|
||||
|
||||
return f"""FROM {base_image}:{config['node_version']}
|
||||
return (
|
||||
f"""FROM {base_image}:{config['node_version']}
|
||||
|
||||
{os.linesep.join(config["dockerfile_lines"])}
|
||||
|
||||
@@ -698,10 +848,14 @@ 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):
|
||||
def config_to_docker(
|
||||
config_path: pathlib.Path, config: Config, base_image: str
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
if config.get("node_version"):
|
||||
return node_config_to_docker(config_path, config, base_image)
|
||||
|
||||
@@ -737,13 +891,24 @@ 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: .
|
||||
context: .{additional_contexts_str}
|
||||
dockerfile_inline: |
|
||||
{textwrap.indent(config_to_docker(config_path, config, base_image), " ")}
|
||||
{textwrap.indent(dockerfile, " ")}
|
||||
{watch_str}
|
||||
"""
|
||||
|
||||
@@ -49,7 +49,9 @@ 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
|
||||
|
||||
Generated
+450
-345
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.1.71"
|
||||
version = "0.1.72"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -14,7 +14,7 @@ langgraph = "langgraph_cli.cli:cli"
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
click = "^8.1.7"
|
||||
langgraph-api = { version = ">=0.0.12,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
langgraph-api = { version = ">=0.0.24,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
python-dotenv = { version = ">=0.8.0", optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -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("./langgraph.json")
|
||||
config_path = pathlib.Path(__file__).parent / "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,18 +129,29 @@ 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
|
||||
ADD . /deps/
|
||||
# -- 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 --
|
||||
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/
|
||||
WORKDIR /deps/cli
|
||||
|
||||
develop:
|
||||
watch:
|
||||
- path: langgraph.json
|
||||
action: rebuild
|
||||
- path: .
|
||||
action: rebuild
|
||||
- path: ../../..
|
||||
action: rebuild\
|
||||
"""
|
||||
assert actual_args == expected_args
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
|
||||
@entrypoint()
|
||||
def graph(state):
|
||||
return None
|
||||
|
||||
@@ -6,10 +6,14 @@
|
||||
],
|
||||
"dependencies": [
|
||||
"langchain_openai",
|
||||
"starlette",
|
||||
"."
|
||||
],
|
||||
"graphs": {
|
||||
"agent": "graphs/agent.py:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
"env": ".env",
|
||||
"http": {
|
||||
"app": "../../examples/my_app.py:app"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ def test_validate_config():
|
||||
"env": {},
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"http": None,
|
||||
**expected_config,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
@@ -50,6 +51,7 @@ def test_validate_config():
|
||||
"env": env,
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"http": None,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
assert actual_config == expected_config
|
||||
@@ -108,6 +110,18 @@ def test_validate_config():
|
||||
}
|
||||
)
|
||||
assert config["python_version"] == "3.12-slim"
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Invalid http.app format",
|
||||
):
|
||||
validate_config(
|
||||
{
|
||||
"python_version": "3.12",
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
"http": {"app": "../../examples/my_app.py"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_validate_config_file():
|
||||
@@ -177,13 +191,27 @@ def test_validate_config_file():
|
||||
# config_to_docker
|
||||
def test_config_to_docker_simple():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||
validate_config(
|
||||
{
|
||||
"dependencies": [".", "../../examples/graphs_reqs_a", "../../examples"],
|
||||
"graphs": graphs,
|
||||
"http": {"app": "../../examples/my_app.py:app"},
|
||||
}
|
||||
),
|
||||
"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 local package ../../examples --
|
||||
COPY --from=examples . /deps/examples
|
||||
# -- End of local package ../../examples --
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -193,16 +221,81 @@ 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 LANGGRAPH_HTTP='{"app": "/deps/examples/my_app.py:app"}'
|
||||
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()
|
||||
),
|
||||
"examples": str((pathlib.Path(__file__).parent / "../../examples").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 = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -216,6 +309,7 @@ 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]' \\
|
||||
@@ -225,11 +319,15 @@ 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():
|
||||
@@ -254,7 +352,7 @@ def test_config_to_docker_invalid_inputs():
|
||||
|
||||
def test_config_to_docker_local_deps():
|
||||
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||
actual_docker_stdin = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -266,6 +364,7 @@ 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]' \\
|
||||
@@ -275,10 +374,14 @@ 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():
|
||||
@@ -291,7 +394,7 @@ dependencies = ["langchain"]"""
|
||||
f.write(pyproject_str)
|
||||
|
||||
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||
actual_docker_stdin = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -303,16 +406,21 @@ 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 = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -330,6 +438,7 @@ 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]' \\
|
||||
@@ -339,15 +448,19 @@ 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 = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -368,6 +481,7 @@ 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
|
||||
@@ -380,6 +494,7 @@ 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]' \\
|
||||
@@ -389,7 +504,10 @@ 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
|
||||
"""
|
||||
@@ -410,6 +528,7 @@ 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]' \\
|
||||
@@ -419,7 +538,10 @@ 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
|
||||
"""
|
||||
@@ -447,6 +569,7 @@ 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]' \\
|
||||
@@ -456,7 +579,10 @@ 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
|
||||
"""
|
||||
@@ -477,6 +603,7 @@ 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]' \\
|
||||
@@ -486,7 +613,10 @@ 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
|
||||
|
||||
@@ -516,6 +646,7 @@ 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]' \\
|
||||
@@ -525,7 +656,10 @@ 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,5 +1,6 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.runnables.config import var_child_runnable_config
|
||||
@@ -9,6 +10,10 @@ 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:
|
||||
@@ -177,4 +182,4 @@ def get_stream_writer() -> StreamWriter:
|
||||
```
|
||||
"""
|
||||
config = get_config()
|
||||
return config[CONF][CONFIG_KEY_STREAM_WRITER]
|
||||
return config[CONF].get(CONFIG_KEY_STREAM_WRITER, _no_op_stream_writer)
|
||||
|
||||
@@ -81,6 +81,8 @@ 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,9 +57,6 @@ 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.
|
||||
@@ -153,10 +150,6 @@ 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,7 +12,11 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from langchain_core.language_models import BaseChatModel, LanguageModelLike
|
||||
from langchain_core.language_models import (
|
||||
BaseChatModel,
|
||||
LanguageModelInput,
|
||||
LanguageModelLike,
|
||||
)
|
||||
from langchain_core.messages import AIMessage, BaseMessage, SystemMessage, ToolMessage
|
||||
from langchain_core.runnables import (
|
||||
Runnable,
|
||||
@@ -52,6 +56,10 @@ class AgentState(TypedDict):
|
||||
|
||||
remaining_steps: RemainingSteps
|
||||
|
||||
|
||||
class AgentStateWithStructuredResponse(AgentState):
|
||||
"""The state of the agent with a structured response."""
|
||||
|
||||
structured_response: StructuredResponse
|
||||
|
||||
|
||||
@@ -63,15 +71,15 @@ PROMPT_RUNNABLE_NAME = "Prompt"
|
||||
MessagesModifier = Union[
|
||||
SystemMessage,
|
||||
str,
|
||||
Callable[[Sequence[BaseMessage]], Sequence[BaseMessage]],
|
||||
Runnable[Sequence[BaseMessage], Sequence[BaseMessage]],
|
||||
Callable[[Sequence[BaseMessage]], LanguageModelInput],
|
||||
Runnable[Sequence[BaseMessage], LanguageModelInput],
|
||||
]
|
||||
|
||||
Prompt = Union[
|
||||
SystemMessage,
|
||||
str,
|
||||
Callable[[StateSchema], Sequence[BaseMessage]],
|
||||
Runnable[StateSchema, Sequence[BaseMessage]],
|
||||
Callable[[StateSchema], LanguageModelInput],
|
||||
Runnable[StateSchema, LanguageModelInput],
|
||||
]
|
||||
|
||||
|
||||
@@ -599,6 +607,13 @@ def create_react_agent(
|
||||
if missing_keys := required_keys - set(state_schema.__annotations__):
|
||||
raise ValueError(f"Missing required key(s) {missing_keys} in state_schema")
|
||||
|
||||
if state_schema is None:
|
||||
state_schema = (
|
||||
AgentStateWithStructuredResponse
|
||||
if response_format is not None
|
||||
else AgentState
|
||||
)
|
||||
|
||||
if isinstance(tools, ToolExecutor):
|
||||
tool_classes: Sequence[BaseTool] = tools.tools
|
||||
tool_node = ToolNode(tool_classes)
|
||||
@@ -748,7 +763,7 @@ def create_react_agent(
|
||||
|
||||
if not tool_calling_enabled:
|
||||
# Define a new graph
|
||||
workflow = StateGraph(state_schema or AgentState)
|
||||
workflow = StateGraph(state_schema)
|
||||
workflow.add_node("agent", RunnableCallable(call_model, acall_model))
|
||||
workflow.set_entry_point("agent")
|
||||
if response_format is not None:
|
||||
|
||||
@@ -59,6 +59,7 @@ 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,
|
||||
@@ -249,25 +250,6 @@ 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]
|
||||
@@ -1755,7 +1737,7 @@ class Pregel(PregelProtocol):
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
submit=loop.submit,
|
||||
submit=config[CONF].get(CONFIG_KEY_RUNNER_SUBMIT, loop.submit),
|
||||
put_writes=loop.put_writes,
|
||||
schedule_task=loop.accept_push,
|
||||
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
|
||||
@@ -2047,7 +2029,7 @@ class Pregel(PregelProtocol):
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
submit=loop.submit,
|
||||
submit=config[CONF].get(CONFIG_KEY_RUNNER_SUBMIT, loop.submit),
|
||||
put_writes=loop.put_writes,
|
||||
schedule_task=loop.accept_push,
|
||||
use_astream=do_stream is not None,
|
||||
|
||||
@@ -323,11 +323,15 @@ class RemoteGraph(PregelProtocol):
|
||||
if k not in reserved_configurable_keys and not k.startswith("__pregel_")
|
||||
}
|
||||
|
||||
return {
|
||||
sanitized: RunnableConfig = {
|
||||
"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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.73"
|
||||
version = "0.2.74"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -2832,10 +2832,10 @@
|
||||
'''
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}, "BaseModel": {"properties": {}, "title": "BaseModel", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "structured_response": {"anyOf": [{"type": "object"}, {"$ref": "#/$defs/BaseModel"}], "title": "Structured Response"}}, "required": ["messages", "structured_response"], "title": "LangGraphInput", "type": "object"}'
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}}, "required": ["messages"], "title": "LangGraphInput", "type": "object"}'
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat.1
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}, "BaseModel": {"properties": {}, "title": "BaseModel", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "structured_response": {"anyOf": [{"type": "object"}, {"$ref": "#/$defs/BaseModel"}], "title": "Structured Response"}}, "required": ["messages", "structured_response"], "title": "LangGraphOutput", "type": "object"}'
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}}, "required": ["messages"], "title": "LangGraphOutput", "type": "object"}'
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat.2
|
||||
'''
|
||||
|
||||
@@ -54,6 +54,7 @@ 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
|
||||
@@ -6495,3 +6496,34 @@ 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",
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -13,4 +13,4 @@ react.d.cts
|
||||
node_modules
|
||||
dist
|
||||
.yarn
|
||||
docs
|
||||
docs
|
||||
|
||||
@@ -15,5 +15,6 @@ export const config = {
|
||||
tsConfigPath: resolve("./tsconfig.json"),
|
||||
cjsSource: "./dist-cjs",
|
||||
cjsDestination: "./dist",
|
||||
additionalGitignorePaths: ["docs"],
|
||||
abs,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.42",
|
||||
"version": "0.0.43",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
@@ -43,6 +43,14 @@
|
||||
"react": "^18 || ^19",
|
||||
"@langchain/core": ">=0.2.31 <0.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"@langchain/core": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": {
|
||||
|
||||
@@ -401,6 +401,7 @@ interface UseStreamOptions<
|
||||
interface UseStream<
|
||||
StateType extends Record<string, unknown> = Record<string, unknown>,
|
||||
UpdateType extends Record<string, unknown> = Partial<StateType>,
|
||||
ConfigurableType extends Record<string, unknown> = Record<string, unknown>,
|
||||
> {
|
||||
/**
|
||||
* The current values of the thread.
|
||||
@@ -425,7 +426,10 @@ interface UseStream<
|
||||
/**
|
||||
* Create and stream a run to the thread.
|
||||
*/
|
||||
submit: (values: UpdateType, options?: SubmitOptions<StateType>) => void;
|
||||
submit: (
|
||||
values: UpdateType,
|
||||
options?: SubmitOptions<StateType, ConfigurableType>,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* The current branch of the thread.
|
||||
@@ -468,10 +472,14 @@ interface UseStream<
|
||||
) => MessageMetadata<StateType> | undefined;
|
||||
}
|
||||
|
||||
type ConfigWithConfigurable<ConfigurableType extends Record<string, unknown>> =
|
||||
Config & { configurable?: ConfigurableType };
|
||||
|
||||
interface SubmitOptions<
|
||||
StateType extends Record<string, unknown> = Record<string, unknown>,
|
||||
ConfigurableType extends Record<string, unknown> = Record<string, unknown>,
|
||||
> {
|
||||
config?: Config;
|
||||
config?: ConfigWithConfigurable<ConfigurableType>;
|
||||
checkpoint?: Omit<Checkpoint, "thread_id"> | null;
|
||||
command?: Command;
|
||||
interruptBefore?: "*" | string[];
|
||||
@@ -490,10 +498,11 @@ interface SubmitOptions<
|
||||
export function useStream<
|
||||
StateType extends Record<string, unknown> = Record<string, unknown>,
|
||||
UpdateType extends Record<string, unknown> = Partial<StateType>,
|
||||
ConfigurableType extends Record<string, unknown> = Record<string, unknown>,
|
||||
CustomType = unknown,
|
||||
>(
|
||||
options: UseStreamOptions<StateType, UpdateType, CustomType>,
|
||||
): UseStream<StateType, UpdateType> {
|
||||
): UseStream<StateType, UpdateType, ConfigurableType> {
|
||||
type EventStreamEvent =
|
||||
| ValuesStreamEvent<StateType>
|
||||
| UpdatesStreamEvent<UpdateType>
|
||||
@@ -640,7 +649,7 @@ export function useStream<
|
||||
|
||||
const submit = async (
|
||||
values: UpdateType | undefined,
|
||||
submitOptions?: SubmitOptions<StateType>,
|
||||
submitOptions?: SubmitOptions<StateType, ConfigurableType>,
|
||||
) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from langgraph_sdk.auth import Auth
|
||||
from langgraph_sdk.client import get_client, get_sync_client
|
||||
from langgraph_sdk.routing import Middleware, Router
|
||||
|
||||
try:
|
||||
from importlib import metadata
|
||||
@@ -9,4 +8,4 @@ try:
|
||||
except metadata.PackageNotFoundError:
|
||||
__version__ = "unknown"
|
||||
|
||||
__all__ = ["Auth", "get_client", "get_sync_client", "Router", "Middleware"]
|
||||
__all__ = ["Auth", "get_client", "get_sync_client"]
|
||||
|
||||
@@ -147,15 +147,21 @@ def get_client(
|
||||
# example usage: client.<model>.<method_name>()
|
||||
assistants = await client.assistants.get(assistant_id="some_uuid")
|
||||
"""
|
||||
|
||||
transport: Optional[httpx.AsyncBaseTransport] = None
|
||||
if url is None:
|
||||
try:
|
||||
from langgraph_api.server import app # type: ignore
|
||||
|
||||
if os.environ.get("__LANGGRAPH_DEFER_LOOPBACK_TRANSPORT") == "true":
|
||||
transport = httpx.ASGITransport(app=None, root_path="/noauth")
|
||||
_registered_transports.append(transport)
|
||||
url = "http://api"
|
||||
transport = httpx.ASGITransport(app, root_path="/noauth")
|
||||
except Exception:
|
||||
url = "http://localhost:8123"
|
||||
else:
|
||||
try:
|
||||
from langgraph_api.server import app # type: ignore
|
||||
|
||||
url = "http://api"
|
||||
transport = httpx.ASGITransport(app, root_path="/noauth")
|
||||
except Exception:
|
||||
url = "http://localhost:8123"
|
||||
|
||||
if transport is None:
|
||||
transport = httpx.AsyncHTTPTransport(retries=5)
|
||||
@@ -1318,9 +1324,9 @@ class RunsClient:
|
||||
""" # noqa: E501
|
||||
payload = {
|
||||
"input": input,
|
||||
"command": {k: v for k, v in command.items() if v is not None}
|
||||
if command
|
||||
else None,
|
||||
"command": (
|
||||
{k: v for k, v in command.items() if v is not None} if command else None
|
||||
),
|
||||
"config": config,
|
||||
"metadata": metadata,
|
||||
"stream_mode": stream_mode,
|
||||
@@ -1505,9 +1511,9 @@ class RunsClient:
|
||||
""" # noqa: E501
|
||||
payload = {
|
||||
"input": input,
|
||||
"command": {k: v for k, v in command.items() if v is not None}
|
||||
if command
|
||||
else None,
|
||||
"command": (
|
||||
{k: v for k, v in command.items() if v is not None} if command else None
|
||||
),
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"config": config,
|
||||
@@ -1676,9 +1682,9 @@ class RunsClient:
|
||||
""" # noqa: E501
|
||||
payload = {
|
||||
"input": input,
|
||||
"command": {k: v for k, v in command.items() if v is not None}
|
||||
if command
|
||||
else None,
|
||||
"command": (
|
||||
{k: v for k, v in command.items() if v is not None} if command else None
|
||||
),
|
||||
"config": config,
|
||||
"metadata": metadata,
|
||||
"assistant_id": assistant_id,
|
||||
@@ -3483,9 +3489,9 @@ class SyncRunsClient:
|
||||
""" # noqa: E501
|
||||
payload = {
|
||||
"input": input,
|
||||
"command": {k: v for k, v in command.items() if v is not None}
|
||||
if command
|
||||
else None,
|
||||
"command": (
|
||||
{k: v for k, v in command.items() if v is not None} if command else None
|
||||
),
|
||||
"config": config,
|
||||
"metadata": metadata,
|
||||
"stream_mode": stream_mode,
|
||||
@@ -3670,9 +3676,9 @@ class SyncRunsClient:
|
||||
""" # noqa: E501
|
||||
payload = {
|
||||
"input": input,
|
||||
"command": {k: v for k, v in command.items() if v is not None}
|
||||
if command
|
||||
else None,
|
||||
"command": (
|
||||
{k: v for k, v in command.items() if v is not None} if command else None
|
||||
),
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"config": config,
|
||||
@@ -3838,9 +3844,9 @@ class SyncRunsClient:
|
||||
""" # noqa: E501
|
||||
payload = {
|
||||
"input": input,
|
||||
"command": {k: v for k, v in command.items() if v is not None}
|
||||
if command
|
||||
else None,
|
||||
"command": (
|
||||
{k: v for k, v in command.items() if v is not None} if command else None
|
||||
),
|
||||
"config": config,
|
||||
"metadata": metadata,
|
||||
"assistant_id": assistant_id,
|
||||
@@ -4444,3 +4450,12 @@ class SyncStoreClient:
|
||||
|
||||
def _provided_vals(d: dict):
|
||||
return {k: v for k, v in d.items() if v is not None}
|
||||
|
||||
|
||||
_registered_transports: list[httpx.ASGITransport] = []
|
||||
|
||||
|
||||
# Do not move; this is used in the server.
|
||||
def configure_loopback_transports(app: Any) -> None:
|
||||
for transport in _registered_transports:
|
||||
transport.app = app
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import typing
|
||||
|
||||
from langgraph_sdk.routing import types
|
||||
from langgraph_sdk.routing.types import Middleware
|
||||
|
||||
|
||||
@typing.final
|
||||
class Router:
|
||||
"""Add routes, middleware, and manage application lifecycle.
|
||||
|
||||
Define custom routes, apply middleware globally, and handle application startup/shutdown.
|
||||
Middleware runs on all routes (including default LangGraph endpoints like /runs/, /assistants/, etc).
|
||||
Custom routes take precedence over default ones, so you can override default behavior if needed.
|
||||
|
||||
???+ example "Basic Usage"
|
||||
```python
|
||||
from contextvars import ContextVar
|
||||
from typing import Any
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.routing import Route
|
||||
from langgraph_sdk import Router, Middleware
|
||||
|
||||
# Enterprise authentication middleware example
|
||||
class JWTAuthMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Any, call_next: Any) -> Any:
|
||||
try:
|
||||
auth_header = request.headers["Authorization"]
|
||||
token = auth_header.split("Bearer ")[1]
|
||||
# Verify JWT token here
|
||||
# Set user context for downstream handlers
|
||||
request.state.user = {"id": "user_123"}
|
||||
response = await call_next(request)
|
||||
return response
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
{"error": "Invalid or missing authentication"},
|
||||
status_code=401
|
||||
)
|
||||
|
||||
# Database connection pool in lifespan
|
||||
async def db_lifespan(app):
|
||||
# Initialize database connection pool
|
||||
from databases import Database
|
||||
database = Database("postgresql://user:pass@localhost/dbname")
|
||||
await database.connect()
|
||||
yield
|
||||
await database.disconnect()
|
||||
|
||||
# Custom login endpoint
|
||||
async def login(request):
|
||||
data = await request.json()
|
||||
# Verify credentials and generate JWT
|
||||
return JSONResponse({
|
||||
"token": "generated.jwt.token"
|
||||
})
|
||||
|
||||
# Protected endpoint example
|
||||
async def protected_route(request):
|
||||
user = request.state.user
|
||||
return JSONResponse({
|
||||
"message": f"Hello {user['id']}"
|
||||
})
|
||||
|
||||
router = Router(
|
||||
middleware=[Middleware(JWTAuthMiddleware)],
|
||||
lifespan=db_lifespan,
|
||||
routes=[
|
||||
Route("/auth/login", endpoint=login, methods=["POST"]),
|
||||
Route("/api/protected", endpoint=protected_route, methods=["GET"])
|
||||
]
|
||||
)
|
||||
|
||||
???+ note "Request Processing Flow"
|
||||
1. Middleware is applied in the order specified, wrapping all routes
|
||||
2. Routes are matched in the following order:
|
||||
* Custom routes defined in the Router take precedence
|
||||
* Default LangGraph routes are used as fallback
|
||||
3. Lifespan manages application startup/shutdown:
|
||||
* Runs before any requests are processed
|
||||
* Ideal for initializing shared resources (DB pools, caches, etc.)
|
||||
* Cleanup occurs during application shutdown
|
||||
|
||||
This allows you to maintain enterprise-grade features while leveraging
|
||||
LangGraph's built-in capabilities.
|
||||
"""
|
||||
|
||||
__slots__ = ("routes", "lifespan", "middleware")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
routes: list[types.BaseRoute],
|
||||
*,
|
||||
lifespan: typing.Union[types.Lifespan[typing.Any], None] = None,
|
||||
middleware: typing.Union[
|
||||
list[
|
||||
typing.Union[
|
||||
types.Middleware,
|
||||
tuple[types._MiddlewareFactory, typing.Any, typing.Any],
|
||||
]
|
||||
],
|
||||
None,
|
||||
] = None,
|
||||
) -> None:
|
||||
self.routes = routes
|
||||
self.lifespan = lifespan
|
||||
self.middleware: list[types.Middleware[typing.Any]] = middleware or []
|
||||
|
||||
|
||||
__all__ = ["Router", "Middleware"]
|
||||
@@ -1,82 +0,0 @@
|
||||
"""Type-hints for the langgraph router.
|
||||
|
||||
Copied from Starlette. When implementing, use Starlette types."""
|
||||
|
||||
import enum
|
||||
import typing
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
|
||||
class Match(enum.Enum):
|
||||
NONE = 0
|
||||
PARTIAL = 1
|
||||
FULL = 2
|
||||
|
||||
|
||||
AppType = typing.TypeVar("AppType")
|
||||
Scope = typing.MutableMapping[str, typing.Any]
|
||||
Message = typing.MutableMapping[str, typing.Any]
|
||||
Receive = typing.Callable[[], typing.Awaitable[Message]]
|
||||
Send = typing.Callable[[Message], typing.Awaitable[None]]
|
||||
|
||||
|
||||
@typing.runtime_checkable
|
||||
class BaseRoute(typing.Protocol):
|
||||
def matches(self, scope: Scope) -> tuple[Match, Scope]:
|
||||
"""Determine if the route matches the given scope."""
|
||||
...
|
||||
|
||||
def url_path_for(self, name: str, /, **path_params: typing.Any) -> str:
|
||||
"""Return the URL path for the given name and path parameters."""
|
||||
...
|
||||
|
||||
async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
"""Handle the event."""
|
||||
...
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
"""Handle the event."""
|
||||
...
|
||||
|
||||
|
||||
StatelessLifespan = typing.Callable[[AppType], typing.AsyncContextManager[None]]
|
||||
StatefulLifespan = typing.Callable[
|
||||
[AppType], typing.AsyncContextManager[typing.Mapping[str, typing.Any]]
|
||||
]
|
||||
Lifespan = typing.Union[StatelessLifespan[AppType], StatefulLifespan[AppType]]
|
||||
ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]
|
||||
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
|
||||
class _MiddlewareFactory(typing.Protocol[P]):
|
||||
def __call__(
|
||||
self, app: ASGIApp, /, *args: P.args, **kwargs: P.kwargs
|
||||
) -> ASGIApp: ... # pragma: no cover
|
||||
|
||||
|
||||
# Copied from Starlette. Basically a named tuple
|
||||
class Middleware:
|
||||
def __init__(
|
||||
self,
|
||||
cls: _MiddlewareFactory[P],
|
||||
*args: P.args,
|
||||
**kwargs: P.kwargs,
|
||||
) -> None:
|
||||
self.cls = cls
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __iter__(self) -> typing.Iterator[typing.Any]:
|
||||
as_tuple = (self.cls, self.args, self.kwargs)
|
||||
return iter(as_tuple)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
class_name = self.__class__.__name__
|
||||
args_strings = [f"{value!r}" for value in self.args]
|
||||
option_strings = [f"{key}={value!r}" for key, value in self.kwargs.items()]
|
||||
name = getattr(self.cls, "__name__", "")
|
||||
args_repr = ", ".join([name] + args_strings + option_strings)
|
||||
return f"{class_name}({args_repr})"
|
||||
@@ -378,11 +378,42 @@ class StreamPart(NamedTuple):
|
||||
|
||||
|
||||
class Send(TypedDict):
|
||||
"""Represents a message to be sent to a specific node in the graph.
|
||||
|
||||
This type is used to explicitly send messages to nodes in the graph, typically
|
||||
used within Command objects to control graph execution flow.
|
||||
"""
|
||||
|
||||
node: str
|
||||
"""The name of the target node to send the message to."""
|
||||
input: Optional[dict[str, Any]]
|
||||
"""Optional dictionary containing the input data to be passed to the node.
|
||||
|
||||
If None, the node will be called with no input."""
|
||||
|
||||
|
||||
class Command(TypedDict, total=False):
|
||||
"""Represents one or more commands to control graph execution flow and state.
|
||||
|
||||
This type defines the control commands that can be returned by nodes to influence
|
||||
graph execution. It lets you navigate to other nodes, update graph state,
|
||||
and resume from interruptions.
|
||||
"""
|
||||
|
||||
goto: Union[Send, str, Sequence[Union[Send, str]]]
|
||||
"""Specifies where execution should continue. Can be:
|
||||
|
||||
- A string node name to navigate to
|
||||
- A Send object to execute a node with specific input
|
||||
- A sequence of node names or Send objects to execute in order
|
||||
"""
|
||||
update: Union[dict[str, Any], Sequence[Tuple[str, Any]]]
|
||||
"""Updates to apply to the graph's state. Can be:
|
||||
|
||||
- A dictionary of state updates to merge
|
||||
- A sequence of (key, value) tuples for ordered updates
|
||||
"""
|
||||
resume: Any
|
||||
"""Value to resume execution with after an interruption.
|
||||
Used in conjunction with interrupt() to implement control flow.
|
||||
"""
|
||||
|
||||
Generated
+123
-127
@@ -2,13 +2,13 @@
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.8.0"
|
||||
version = "4.7.0"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"},
|
||||
{file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"},
|
||||
{file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"},
|
||||
{file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -19,29 +19,29 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
|
||||
trio = ["trio (>=0.26.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.1.31"
|
||||
version = "2024.8.30"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
|
||||
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
|
||||
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
|
||||
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespell"
|
||||
version = "2.4.1"
|
||||
description = "Fix common misspellings in text files"
|
||||
version = "2.3.0"
|
||||
description = "Codespell"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425"},
|
||||
{file = "codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5"},
|
||||
{file = "codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1"},
|
||||
{file = "codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -168,49 +168,49 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.15.0"
|
||||
version = "1.13.0"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"},
|
||||
{file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"},
|
||||
{file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"},
|
||||
{file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
|
||||
{file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
|
||||
{file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
|
||||
{file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
|
||||
{file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
|
||||
{file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
|
||||
{file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
|
||||
{file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
|
||||
{file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
|
||||
{file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
|
||||
{file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
|
||||
{file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
|
||||
{file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
|
||||
{file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
|
||||
{file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
|
||||
{file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
|
||||
{file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
|
||||
{file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
|
||||
{file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
|
||||
{file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
|
||||
{file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
|
||||
{file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
|
||||
{file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
|
||||
{file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
|
||||
{file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
|
||||
{file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
|
||||
{file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
|
||||
{file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
|
||||
{file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
|
||||
{file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
|
||||
{file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
|
||||
{file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mypy_extensions = ">=1.0.0"
|
||||
mypy-extensions = ">=1.0.0"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
typing_extensions = ">=4.6.0"
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[package.extras]
|
||||
dmypy = ["psutil (>=4.0)"]
|
||||
@@ -232,90 +232,86 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.10.15"
|
||||
version = "3.10.12"
|
||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e8afd6200e12771467a1a44e5ad780614b86abb4b11862ec54861a82d677746"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9a18c500f19273e9e104cca8c1f0b40a6470bcccfc33afcc088045d0bf5ea6"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb00b7bfbdf5d34a13180e4805d76b4567025da19a197645ca746fc2fb536586"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33aedc3d903378e257047fee506f11e0833146ca3e57a1a1fb0ddb789876c1e1"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0099ae6aed5eb1fc84c9eb72b95505a3df4267e6962eb93cdd5af03be71c98"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c864a80a2d467d7786274fce0e4f93ef2a7ca4ff31f7fc5634225aaa4e9e98c"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c25774c9e88a3e0013d7d1a6c8056926b607a61edd423b50eb5c88fd7f2823ae"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e78c211d0074e783d824ce7bb85bf459f93a233eb67a5b5003498232ddfb0e8a"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:43e17289ffdbbac8f39243916c893d2ae41a2ea1a9cbb060a56a4d75286351ae"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:781d54657063f361e89714293c095f506c533582ee40a426cb6489c48a637b81"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6875210307d36c94873f553786a808af2788e362bd0cf4c8e66d976791e7b528"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-win32.whl", hash = "sha256:305b38b2b8f8083cc3d618927d7f424349afce5975b316d33075ef0f73576b60"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:5dd9ef1639878cc3efffed349543cbf9372bdbd79f478615a1c633fe4e4180d1"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ffe19f3e8d68111e8644d4f4e267a069ca427926855582ff01fc012496d19969"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d433bf32a363823863a96561a555227c18a522a8217a6f9400f00ddc70139ae2"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da03392674f59a95d03fa5fb9fe3a160b0511ad84b7a3914699ea5a1b3a38da2"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a63bb41559b05360ded9132032239e47983a39b151af1201f07ec9370715c82"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3766ac4702f8f795ff3fa067968e806b4344af257011858cc3d6d8721588b53f"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1c73dcc8fadbd7c55802d9aa093b36878d34a3b3222c41052ce6b0fc65f8e8"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b299383825eafe642cbab34be762ccff9fd3408d72726a6b2a4506d410a71ab3"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:abc7abecdbf67a173ef1316036ebbf54ce400ef2300b4e26a7b843bd446c2480"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3614ea508d522a621384c1d6639016a5a2e4f027f3e4a1c93a51867615d28829"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:295c70f9dc154307777ba30fe29ff15c1bcc9dfc5c48632f37d20a607e9ba85a"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:63309e3ff924c62404923c80b9e2048c1f74ba4b615e7584584389ada50ed428"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-win32.whl", hash = "sha256:a2f708c62d026fb5340788ba94a55c23df4e1869fec74be455e0b2f5363b8507"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:efcf6c735c3d22ef60c4aa27a5238f1a477df85e9b15f2142f9d669beb2d13fd"},
|
||||
{file = "orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e"},
|
||||
{file = "orjson-3.10.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ece01a7ec71d9940cc654c482907a6b65df27251255097629d0dea781f255c6d"},
|
||||
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34ec9aebc04f11f4b978dd6caf697a2df2dd9b47d35aa4cc606cabcb9df69d7"},
|
||||
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6ec8658da3480939c79b9e9e27e0db31dffcd4ba69c334e98c9976ac29140e"},
|
||||
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17e6baf4cf01534c9de8a16c0c611f3d94925d1701bf5f4aff17003677d8ced"},
|
||||
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6402ebb74a14ef96f94a868569f5dccf70d791de49feb73180eb3c6fda2ade56"},
|
||||
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0000758ae7c7853e0a4a6063f534c61656ebff644391e1f81698c1b2d2fc8cd2"},
|
||||
{file = "orjson-3.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:888442dcee99fd1e5bd37a4abb94930915ca6af4db50e23e746cdf4d1e63db13"},
|
||||
{file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1f7a3ce79246aa0e92f5458d86c54f257fb5dfdc14a192651ba7ec2c00f8a05"},
|
||||
{file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:802a3935f45605c66fb4a586488a38af63cb37aaad1c1d94c982c40dcc452e85"},
|
||||
{file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1da1ef0113a2be19bb6c557fb0ec2d79c92ebd2fed4cfb1b26bab93f021fb885"},
|
||||
{file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a3273e99f367f137d5b3fecb5e9f45bcdbfac2a8b2f32fbc72129bbd48789c2"},
|
||||
{file = "orjson-3.10.12-cp310-none-win32.whl", hash = "sha256:475661bf249fd7907d9b0a2a2421b4e684355a77ceef85b8352439a9163418c3"},
|
||||
{file = "orjson-3.10.12-cp310-none-win_amd64.whl", hash = "sha256:87251dc1fb2b9e5ab91ce65d8f4caf21910d99ba8fb24b49fd0c118b2362d509"},
|
||||
{file = "orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74"},
|
||||
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23"},
|
||||
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252"},
|
||||
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef"},
|
||||
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252"},
|
||||
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4"},
|
||||
{file = "orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae"},
|
||||
{file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b"},
|
||||
{file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da"},
|
||||
{file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07"},
|
||||
{file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd"},
|
||||
{file = "orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79"},
|
||||
{file = "orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8"},
|
||||
{file = "orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d"},
|
||||
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f"},
|
||||
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70"},
|
||||
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69"},
|
||||
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9"},
|
||||
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192"},
|
||||
{file = "orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559"},
|
||||
{file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc"},
|
||||
{file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f"},
|
||||
{file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be"},
|
||||
{file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c"},
|
||||
{file = "orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708"},
|
||||
{file = "orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb"},
|
||||
{file = "orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543"},
|
||||
{file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296"},
|
||||
{file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e"},
|
||||
{file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f"},
|
||||
{file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e"},
|
||||
{file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6"},
|
||||
{file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e"},
|
||||
{file = "orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc"},
|
||||
{file = "orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825"},
|
||||
{file = "orjson-3.10.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7d69af5b54617a5fac5c8e5ed0859eb798e2ce8913262eb522590239db6c6763"},
|
||||
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ed119ea7d2953365724a7059231a44830eb6bbb0cfead33fcbc562f5fd8f935"},
|
||||
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5fc1238ef197e7cad5c91415f524aaa51e004be5a9b35a1b8a84ade196f73f"},
|
||||
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43509843990439b05f848539d6f6198d4ac86ff01dd024b2f9a795c0daeeab60"},
|
||||
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f72e27a62041cfb37a3de512247ece9f240a561e6c8662276beaf4d53d406db4"},
|
||||
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a904f9572092bb6742ab7c16c623f0cdccbad9eeb2d14d4aa06284867bddd31"},
|
||||
{file = "orjson-3.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:855c0833999ed5dc62f64552db26f9be767434917d8348d77bacaab84f787d7b"},
|
||||
{file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:897830244e2320f6184699f598df7fb9db9f5087d6f3f03666ae89d607e4f8ed"},
|
||||
{file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0b32652eaa4a7539f6f04abc6243619c56f8530c53bf9b023e1269df5f7816dd"},
|
||||
{file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:36b4aa31e0f6a1aeeb6f8377769ca5d125db000f05c20e54163aef1d3fe8e833"},
|
||||
{file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5535163054d6cbf2796f93e4f0dbc800f61914c0e3c4ed8499cf6ece22b4a3da"},
|
||||
{file = "orjson-3.10.12-cp38-none-win32.whl", hash = "sha256:90a5551f6f5a5fa07010bf3d0b4ca2de21adafbbc0af6cb700b63cd767266cb9"},
|
||||
{file = "orjson-3.10.12-cp38-none-win_amd64.whl", hash = "sha256:703a2fb35a06cdd45adf5d733cf613cbc0cb3ae57643472b16bc22d325b5fb6c"},
|
||||
{file = "orjson-3.10.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f29de3ef71a42a5822765def1febfb36e0859d33abf5c2ad240acad5c6a1b78d"},
|
||||
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de365a42acc65d74953f05e4772c974dad6c51cfc13c3240899f534d611be967"},
|
||||
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a5a0158648a67ff0004cb0df5df7dcc55bfc9ca154d9c01597a23ad54c8d0c"},
|
||||
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c47ce6b8d90fe9646a25b6fb52284a14ff215c9595914af63a5933a49972ce36"},
|
||||
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0eee4c2c5bfb5c1b47a5db80d2ac7aaa7e938956ae88089f098aff2c0f35d5d8"},
|
||||
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d3081bbe8b86587eb5c98a73b97f13d8f9fea685cf91a579beddacc0d10566"},
|
||||
{file = "orjson-3.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c23a6e90383884068bc2dba83d5222c9fcc3b99a0ed2411d38150734236755"},
|
||||
{file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5472be7dc3269b4b52acba1433dac239215366f89dc1d8d0e64029abac4e714e"},
|
||||
{file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7319cda750fca96ae5973efb31b17d97a5c5225ae0bc79bf5bf84df9e1ec2ab6"},
|
||||
{file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:74d5ca5a255bf20b8def6a2b96b1e18ad37b4a122d59b154c458ee9494377f80"},
|
||||
{file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ff31d22ecc5fb85ef62c7d4afe8301d10c558d00dd24274d4bbe464380d3cd69"},
|
||||
{file = "orjson-3.10.12-cp39-none-win32.whl", hash = "sha256:c22c3ea6fba91d84fcb4cda30e64aff548fcf0c44c876e681f47d61d24b12e6b"},
|
||||
{file = "orjson-3.10.12-cp39-none-win_amd64.whl", hash = "sha256:be604f60d45ace6b0b33dd990a66b4526f1a7a186ac411c942674625456ca548"},
|
||||
{file = "orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.52"
|
||||
version = "0.1.53"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user