mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-06 08:25:14 +02:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4de4abbe29 | ||
|
|
834fa8932f | ||
|
|
9458700fe9 | ||
|
|
122a63fc83 | ||
|
|
d05eac67f8 | ||
|
|
31b5a9c4fe |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph"
|
||||
version = "1.0.7"
|
||||
version = "1.0.6"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
@@ -27,7 +27,7 @@ dependencies = [
|
||||
"langchain-core>=0.1",
|
||||
"langgraph-checkpoint>=2.1.0,<5.0.0",
|
||||
"langgraph-sdk>=0.3.0,<0.4.0",
|
||||
"langgraph-prebuilt>=1.0.7,<1.1.0",
|
||||
"langgraph-prebuilt>=1.0.2,<1.1.0",
|
||||
"xxhash>=3.5.0",
|
||||
"pydantic>=2.7.4",
|
||||
]
|
||||
|
||||
Generated
+77
-77
@@ -192,7 +192,7 @@ name = "blockbuster"
|
||||
version = "1.5.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "forbiddenfruit", marker = "python_full_version >= '3.11' and python_full_version < '3.14' and implementation_name == 'cpython'" },
|
||||
{ name = "forbiddenfruit", marker = "python_full_version >= '3.11' and implementation_name == 'cpython'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e0/dcbab602790a576b0b94108c07e2c048e5897df7cc83722a89582d733987/blockbuster-1.5.26.tar.gz", hash = "sha256:cc3ce8c70fa852a97ee3411155f31e4ad2665cd1c6c7d2f8bb1851dab61dc629", size = 36085, upload-time = "2025-12-05T10:43:47.735Z" }
|
||||
wheels = [
|
||||
@@ -384,7 +384,7 @@ name = "click"
|
||||
version = "8.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||
wheels = [
|
||||
@@ -527,7 +527,7 @@ name = "cryptography"
|
||||
version = "46.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "python_full_version >= '3.11' and python_full_version < '3.14' and platform_python_implementation != 'PyPy'" },
|
||||
{ name = "cffi", marker = "python_full_version >= '3.11' and platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
|
||||
wheels = [
|
||||
@@ -692,7 +692,7 @@ name = "googleapis-common-protos"
|
||||
version = "1.72.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" }
|
||||
wheels = [
|
||||
@@ -704,7 +704,7 @@ name = "grpcio"
|
||||
version = "1.76.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
|
||||
wheels = [
|
||||
@@ -765,8 +765,8 @@ name = "grpcio-health-checking"
|
||||
version = "1.76.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "grpcio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "grpcio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3e/96/5a52dcf21078b47ffa0c2ed613c3153a06f138edb6133792bace5f1ccc1d/grpcio_health_checking-1.76.0.tar.gz", hash = "sha256:b7a99d74096b3ab3a59987fc02374068e1c180a352e8d1f79f10e5a23727098d", size = 16784, upload-time = "2025-10-21T16:28:55.204Z" }
|
||||
wheels = [
|
||||
@@ -778,9 +778,9 @@ name = "grpcio-tools"
|
||||
version = "1.75.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "grpcio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "setuptools", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "grpcio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "setuptools", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/76/0cd2a2bb379275c319544a3ab613dc3cea7a167503908c1b4de55f82bd9e/grpcio_tools-1.75.1.tar.gz", hash = "sha256:bb78960cf3d58941e1fec70cbdaccf255918beed13c34112a6915a6d8facebd1", size = 5390470, upload-time = "2025-09-26T09:10:11.948Z" }
|
||||
wheels = [
|
||||
@@ -887,7 +887,7 @@ name = "importlib-metadata"
|
||||
version = "8.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "zipp", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "zipp", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
|
||||
wheels = [
|
||||
@@ -1372,7 +1372,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.7"
|
||||
version = "1.0.6"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -1518,33 +1518,33 @@ name = "langgraph-api"
|
||||
version = "0.6.39"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cloudpickle", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "cryptography", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "grpcio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "grpcio-health-checking", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "grpcio-tools", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "httpx", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "jsonschema-rs", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langchain-core", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langsmith", extra = ["otel"], marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "orjson", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "pyjwt", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "sse-starlette", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "structlog", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "tenacity", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "truststore", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "uuid-utils", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "uvicorn", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "watchfiles", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "cloudpickle", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "cryptography", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "grpcio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "grpcio-health-checking", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "grpcio-tools", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "httpx", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "jsonschema-rs", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langchain-core", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langsmith", extra = ["otel"], marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "orjson", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "pyjwt", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sse-starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "structlog", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "tenacity", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "truststore", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "uuid-utils", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "uvicorn", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "watchfiles", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/52/f7d25b475768e5e0748d4725c64909fc7e56a714e064976a32e7c68f6d9e/langgraph_api-0.6.39.tar.gz", hash = "sha256:052191ec9c49d76b0d5592a60fe6324eafde40e7a1afba65d1ce11a6a074674b", size = 447880, upload-time = "2026-01-16T19:54:07.475Z" }
|
||||
wheels = [
|
||||
@@ -1692,15 +1692,15 @@ test = [
|
||||
name = "langgraph-cli"
|
||||
source = { editable = "../cli" }
|
||||
dependencies = [
|
||||
{ name = "click", marker = "python_full_version < '3.14'" },
|
||||
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "click" },
|
||||
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
inmem = [
|
||||
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "python-dotenv", marker = "python_full_version < '3.14'" },
|
||||
{ name = "langgraph-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -1740,7 +1740,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.7"
|
||||
version = "1.0.6"
|
||||
source = { editable = "../prebuilt" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -1794,12 +1794,12 @@ name = "langgraph-runtime-inmem"
|
||||
version = "0.22.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blockbuster", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "sse-starlette", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "structlog", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "blockbuster", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sse-starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "structlog", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/8d/4b42a852a869c6a60aceae4e6157030c427ae9c07bf28769cc8e2fcec16b/langgraph_runtime_inmem-0.22.1.tar.gz", hash = "sha256:bbd983975dc372ad5cd5288263d3471732b0dff2bab3c6f41e5fe5b3a38ea1ee", size = 104172, upload-time = "2026-01-16T15:17:23.381Z" }
|
||||
wheels = [
|
||||
@@ -1868,9 +1868,9 @@ wheels = [
|
||||
|
||||
[package.optional-dependencies]
|
||||
otel = [
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2216,8 +2216,8 @@ name = "opentelemetry-api"
|
||||
version = "1.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "importlib-metadata", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" }
|
||||
wheels = [
|
||||
@@ -2229,7 +2229,7 @@ name = "opentelemetry-exporter-otlp-proto-common"
|
||||
version = "1.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" }
|
||||
wheels = [
|
||||
@@ -2241,13 +2241,13 @@ name = "opentelemetry-exporter-otlp-proto-http"
|
||||
version = "1.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "googleapis-common-protos", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "requests", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "googleapis-common-protos", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "requests", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" }
|
||||
wheels = [
|
||||
@@ -2259,7 +2259,7 @@ name = "opentelemetry-proto"
|
||||
version = "1.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" }
|
||||
wheels = [
|
||||
@@ -2271,9 +2271,9 @@ name = "opentelemetry-sdk"
|
||||
version = "1.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" }
|
||||
wheels = [
|
||||
@@ -2285,8 +2285,8 @@ name = "opentelemetry-semantic-conventions"
|
||||
version = "0.60b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" }
|
||||
wheels = [
|
||||
@@ -3469,9 +3469,9 @@ name = "sse-starlette"
|
||||
version = "2.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "uvicorn", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "uvicorn", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678, upload-time = "2024-08-01T08:52:50.248Z" }
|
||||
wheels = [
|
||||
@@ -3497,7 +3497,7 @@ name = "starlette"
|
||||
version = "0.51.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/65/5a1fadcc40c5fdc7df421a7506b79633af8f5d5e3a95c3e72acacec644b9/starlette-0.51.0.tar.gz", hash = "sha256:4c4fda9b1bc67f84037d3d14a5112e523509c369d9d47b111b2f984b0cc5ba6c", size = 2647658, upload-time = "2026-01-10T20:23:15.043Z" }
|
||||
@@ -3746,8 +3746,8 @@ name = "uvicorn"
|
||||
version = "0.40.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "h11", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "click", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "h11", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" }
|
||||
wheels = [
|
||||
@@ -3823,7 +3823,7 @@ name = "watchfiles"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
|
||||
wheels = [
|
||||
|
||||
@@ -287,7 +287,10 @@ def create_react_agent(
|
||||
[StateSchema, Runtime[ContextT]],
|
||||
Awaitable[Runnable[LanguageModelInput, BaseMessage]],
|
||||
],
|
||||
tools: Sequence[BaseTool | Callable | dict[str, Any]] | ToolNode,
|
||||
tools: Sequence[BaseTool | Callable | dict[str, Any]]
|
||||
| Callable[[], Sequence[BaseTool]]
|
||||
| Callable[[], Awaitable[Sequence[BaseTool]]]
|
||||
| ToolNode,
|
||||
*,
|
||||
prompt: Prompt | None = None,
|
||||
response_format: StructuredResponseSchema
|
||||
@@ -355,8 +358,9 @@ def create_react_agent(
|
||||
`.bind_tools()` and support required functionality. Bound tools
|
||||
must be a subset of those specified in the `tools` parameter.
|
||||
|
||||
tools: A list of tools or a `ToolNode` instance.
|
||||
tools: A list of tools, a `ToolNode` instance, or a callable that returns tools.
|
||||
If an empty list is provided, the agent will consist of a single LLM node without tool calling.
|
||||
Callable tools providers (both sync and async) enable dynamic tool selection at runtime.
|
||||
prompt: An optional prompt for the LLM. Can take a few different forms:
|
||||
|
||||
- `str`: This is converted to a `SystemMessage` and added to the beginning of the list of messages in `state["messages"]`.
|
||||
@@ -546,18 +550,30 @@ def create_react_agent(
|
||||
)
|
||||
|
||||
llm_builtin_tools: list[dict] = []
|
||||
is_dynamic_tools = callable(tools) and not isinstance(tools, ToolNode)
|
||||
if isinstance(tools, ToolNode):
|
||||
tool_classes = list(tools.tools_by_name.values())
|
||||
tool_node = tools
|
||||
elif is_dynamic_tools:
|
||||
# Dynamic tools provider - pass directly to ToolNode
|
||||
tool_node = ToolNode(
|
||||
cast(
|
||||
"Callable[[], Sequence[BaseTool]] | Callable[[], Awaitable[Sequence[BaseTool]]]",
|
||||
tools,
|
||||
)
|
||||
)
|
||||
# For dynamic tools, we can't know the tools at compile time
|
||||
tool_classes = []
|
||||
else:
|
||||
llm_builtin_tools = [t for t in tools if isinstance(t, dict)]
|
||||
tool_node = ToolNode([t for t in tools if not isinstance(t, dict)])
|
||||
tools_seq = cast("Sequence[BaseTool | Callable | dict[str, Any]]", tools)
|
||||
llm_builtin_tools = [t for t in tools_seq if isinstance(t, dict)]
|
||||
tool_node = ToolNode([t for t in tools_seq if not isinstance(t, dict)])
|
||||
tool_classes = list(tool_node.tools_by_name.values())
|
||||
|
||||
is_dynamic_model = not isinstance(model, (str, Runnable)) and callable(model)
|
||||
is_async_dynamic_model = is_dynamic_model and inspect.iscoroutinefunction(model)
|
||||
|
||||
tool_calling_enabled = len(tool_classes) > 0
|
||||
tool_calling_enabled = len(tool_classes) > 0 or is_dynamic_tools
|
||||
|
||||
if not is_dynamic_model:
|
||||
if isinstance(model, str):
|
||||
|
||||
@@ -42,7 +42,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import inspect
|
||||
import json
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from copy import copy, deepcopy
|
||||
from dataclasses import dataclass, replace
|
||||
from types import UnionType
|
||||
@@ -90,8 +90,6 @@ from pydantic import BaseModel, ValidationError
|
||||
from typing_extensions import TypeVar, Unpack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
from langgraph.runtime import Runtime
|
||||
from pydantic_core import ErrorDetails
|
||||
|
||||
@@ -121,7 +119,6 @@ class _ToolCallRequestOverrides(TypedDict, total=False):
|
||||
"""Possible overrides for ToolCallRequest.override() method."""
|
||||
|
||||
tool_call: ToolCall
|
||||
tool: BaseTool
|
||||
state: Any
|
||||
|
||||
|
||||
@@ -654,9 +651,25 @@ class ToolNode(RunnableCallable):
|
||||
- `Command` can update state, trigger navigation, or send messages
|
||||
|
||||
Args:
|
||||
tools: A sequence of tools that can be invoked by this node.
|
||||
tools: Tools that can be invoked by this node. Can be either:
|
||||
|
||||
Supports:
|
||||
- **A sequence of tools**: A list/tuple of tools (static)
|
||||
- **A callable**: A function that returns a sequence of tools (dynamic).
|
||||
The callable is invoked on every `invoke()` or `ainvoke()` call,
|
||||
allowing the available tools to change between invocations.
|
||||
|
||||
**Invocation semantics for dynamic tools callables:**
|
||||
|
||||
- Called once at the start of each ToolNode invocation
|
||||
- Should return a consistent set of tools for a given invocation
|
||||
(i.e., idempotent within a single invocation)
|
||||
- Stochastic behavior (returning different tools across invocations)
|
||||
is supported but be aware the model may reference tools that are
|
||||
no longer available
|
||||
- Must return `BaseTool` instances (not plain callables) to avoid
|
||||
expensive introspection on every invocation
|
||||
|
||||
Each tool in the sequence supports:
|
||||
|
||||
- **BaseTool instances**: Tools with schemas and metadata
|
||||
- **Plain functions**: Automatically converted to tools with inferred schemas
|
||||
@@ -736,7 +749,9 @@ class ToolNode(RunnableCallable):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tools: Sequence[BaseTool | Callable],
|
||||
tools: Sequence[BaseTool | Callable]
|
||||
| Callable[[], Sequence[BaseTool]]
|
||||
| Callable[[], Awaitable[Sequence[BaseTool]]],
|
||||
*,
|
||||
name: str = "tools",
|
||||
tags: list[str] | None = None,
|
||||
@@ -752,7 +767,8 @@ class ToolNode(RunnableCallable):
|
||||
"""Initialize `ToolNode` with tools and configuration.
|
||||
|
||||
Args:
|
||||
tools: Sequence of tools to make available for execution.
|
||||
tools: Tools to make available for execution. Can be a sequence of tools
|
||||
or a callable that returns a sequence of tools (for dynamic tools).
|
||||
name: Node name for graph identification.
|
||||
tags: Optional metadata tags.
|
||||
handle_tool_errors: Error handling configuration.
|
||||
@@ -764,25 +780,117 @@ class ToolNode(RunnableCallable):
|
||||
If not provided, falls back to wrap_tool_call for async execution.
|
||||
"""
|
||||
super().__init__(self._func, self._afunc, name=name, tags=tags, trace=False)
|
||||
self._tools_by_name: dict[str, BaseTool] = {}
|
||||
self._injected_args: dict[str, _InjectedArgs] = {}
|
||||
self._handle_tool_errors = handle_tool_errors
|
||||
self._messages_key = messages_key
|
||||
self._wrap_tool_call = wrap_tool_call
|
||||
self._awrap_tool_call = awrap_tool_call
|
||||
|
||||
self._tools_provider: Callable[[], Sequence[BaseTool]] | None = None
|
||||
self._async_tools_provider: (
|
||||
Callable[[], Awaitable[Sequence[BaseTool]]] | None
|
||||
) = None
|
||||
self._tools_by_name: dict[str, BaseTool] = {}
|
||||
|
||||
if callable(tools) and not isinstance(tools, (list, tuple)):
|
||||
# It's a dynamic tools provider
|
||||
if inspect.iscoroutinefunction(tools):
|
||||
self._async_tools_provider = cast(
|
||||
"Callable[[], Awaitable[Sequence[BaseTool]]]", tools
|
||||
)
|
||||
else:
|
||||
self._tools_provider = cast("Callable[[], Sequence[BaseTool]]", tools)
|
||||
else:
|
||||
# It's a sequence of tools - process them statically
|
||||
self._tools_by_name = self._build_tools_mapping(tools)
|
||||
|
||||
def _build_tools_mapping(
|
||||
self,
|
||||
tools: Sequence[BaseTool | Callable],
|
||||
*,
|
||||
convert_callables: bool = True,
|
||||
) -> dict[str, BaseTool]:
|
||||
"""Build tools_by_name mapping from a sequence of tools.
|
||||
|
||||
Args:
|
||||
tools: Sequence of tools to process.
|
||||
convert_callables: Whether to convert plain callables to BaseTools.
|
||||
Set to False when processing tools from a dynamic provider
|
||||
(which should already be BaseTools).
|
||||
|
||||
Returns:
|
||||
Dictionary mapping tool names to BaseTool instances.
|
||||
"""
|
||||
tools_by_name: dict[str, BaseTool] = {}
|
||||
for tool in tools:
|
||||
if not isinstance(tool, BaseTool):
|
||||
tool_ = create_tool(cast("type[BaseTool]", tool))
|
||||
if convert_callables:
|
||||
tool_ = create_tool(cast("type[BaseTool]", tool))
|
||||
else:
|
||||
# Dynamic tools providers must return BaseTool instances, not plain
|
||||
# callables. Converting callables to tools requires calling
|
||||
# create_tool() which performs introspection. Doing this on every
|
||||
# invocation would be expensive and could have unexpected side effects.
|
||||
# Users should convert their callables to tools once upfront.
|
||||
msg = (
|
||||
f"Dynamic tools provider must return BaseTool instances, "
|
||||
f"got {type(tool).__name__}"
|
||||
)
|
||||
raise TypeError(msg)
|
||||
else:
|
||||
tool_ = tool
|
||||
self._tools_by_name[tool_.name] = tool_
|
||||
# Build injected args mapping once during initialization in a single pass
|
||||
self._injected_args[tool_.name] = _get_all_injected_args(tool_)
|
||||
tools_by_name[tool_.name] = tool_
|
||||
return tools_by_name
|
||||
|
||||
def _get_tools(self) -> dict[str, BaseTool]:
|
||||
"""Get the current tools mapping.
|
||||
|
||||
If a tools provider was configured, calls it to get the current tools.
|
||||
Otherwise, returns the statically configured tools.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping tool names to BaseTool instances.
|
||||
|
||||
Raises:
|
||||
TypeError: If an async tools provider is used in synchronous context.
|
||||
"""
|
||||
if self._async_tools_provider is not None:
|
||||
msg = (
|
||||
"Cannot use async tools provider in synchronous context. "
|
||||
"Use ainvoke() instead of invoke()."
|
||||
)
|
||||
raise TypeError(msg)
|
||||
if self._tools_provider is not None:
|
||||
tools = self._tools_provider()
|
||||
return self._build_tools_mapping(tools, convert_callables=False)
|
||||
return self._tools_by_name
|
||||
|
||||
async def _aget_tools(self) -> dict[str, BaseTool]:
|
||||
"""Get the current tools mapping asynchronously.
|
||||
|
||||
If an async or sync tools provider was configured, calls it to get
|
||||
the current tools. Otherwise, returns the statically configured tools.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping tool names to BaseTool instances.
|
||||
"""
|
||||
if self._async_tools_provider is not None:
|
||||
tools = await self._async_tools_provider()
|
||||
return self._build_tools_mapping(tools, convert_callables=False)
|
||||
if self._tools_provider is not None:
|
||||
tools = self._tools_provider()
|
||||
return self._build_tools_mapping(tools, convert_callables=False)
|
||||
return self._tools_by_name
|
||||
|
||||
@property
|
||||
def tools_by_name(self) -> dict[str, BaseTool]:
|
||||
"""Mapping from tool name to BaseTool instance."""
|
||||
return self._tools_by_name
|
||||
"""Mapping from tool name to BaseTool instance.
|
||||
|
||||
Note: If a sync dynamic tools provider was configured, this property
|
||||
calls the provider to get the current tools on each access.
|
||||
If an async tools provider was configured, this property will raise
|
||||
a TypeError - use ainvoke() instead.
|
||||
"""
|
||||
return self._get_tools()
|
||||
|
||||
def _func(
|
||||
self,
|
||||
@@ -793,6 +901,9 @@ class ToolNode(RunnableCallable):
|
||||
tool_calls, input_type = self._parse_input(input)
|
||||
config_list = get_config_list(config, len(tool_calls))
|
||||
|
||||
# Get tools once at the start of invocation (supports dynamic tools)
|
||||
tools_by_name = self._get_tools()
|
||||
|
||||
# Construct ToolRuntime instances at the top level for each tool call
|
||||
tool_runtimes = []
|
||||
for call, cfg in zip(tool_calls, config_list, strict=False):
|
||||
@@ -807,11 +918,17 @@ class ToolNode(RunnableCallable):
|
||||
)
|
||||
tool_runtimes.append(tool_runtime)
|
||||
|
||||
# Pass original tool calls without injection
|
||||
def run_one_with_tools(
|
||||
call: ToolCall,
|
||||
input_type: Literal["list", "dict", "tool_calls"],
|
||||
tool_runtime: ToolRuntime,
|
||||
) -> ToolMessage | Command:
|
||||
return self._run_one(call, input_type, tool_runtime, tools_by_name)
|
||||
|
||||
input_types = [input_type] * len(tool_calls)
|
||||
with get_executor_for_config(config) as executor:
|
||||
outputs = list(
|
||||
executor.map(self._run_one, tool_calls, input_types, tool_runtimes)
|
||||
executor.map(run_one_with_tools, tool_calls, input_types, tool_runtimes)
|
||||
)
|
||||
|
||||
return self._combine_tool_outputs(outputs, input_type)
|
||||
@@ -825,6 +942,9 @@ class ToolNode(RunnableCallable):
|
||||
tool_calls, input_type = self._parse_input(input)
|
||||
config_list = get_config_list(config, len(tool_calls))
|
||||
|
||||
# Get tools once at the start of invocation (supports dynamic tools)
|
||||
tools_by_name = await self._aget_tools()
|
||||
|
||||
# Construct ToolRuntime instances at the top level for each tool call
|
||||
tool_runtimes = []
|
||||
for call, cfg in zip(tool_calls, config_list, strict=False):
|
||||
@@ -839,10 +959,10 @@ class ToolNode(RunnableCallable):
|
||||
)
|
||||
tool_runtimes.append(tool_runtime)
|
||||
|
||||
# Pass original tool calls without injection
|
||||
coros = []
|
||||
for call, tool_runtime in zip(tool_calls, tool_runtimes, strict=False):
|
||||
coros.append(self._arun_one(call, input_type, tool_runtime)) # type: ignore[arg-type]
|
||||
coros = [
|
||||
self._arun_one(call, input_type, tool_runtime, tools_by_name) # type: ignore[arg-type]
|
||||
for call, tool_runtime in zip(tool_calls, tool_runtimes, strict=False)
|
||||
]
|
||||
outputs = await asyncio.gather(*coros)
|
||||
|
||||
return self._combine_tool_outputs(outputs, input_type)
|
||||
@@ -896,13 +1016,17 @@ class ToolNode(RunnableCallable):
|
||||
request: ToolCallRequest,
|
||||
input_type: Literal["list", "dict", "tool_calls"],
|
||||
config: RunnableConfig,
|
||||
tools_by_name: dict[str, BaseTool],
|
||||
) -> ToolMessage | Command:
|
||||
"""Execute tool call with configured error handling.
|
||||
|
||||
Args:
|
||||
request: Tool execution request.
|
||||
request: Tool execution request (includes the specific tool to execute).
|
||||
input_type: Input format.
|
||||
config: Runnable configuration.
|
||||
tools_by_name: Mapping from tool name to BaseTool. Used only for
|
||||
validation error messages when the requested tool doesn't exist,
|
||||
to list available tools in the error response.
|
||||
|
||||
Returns:
|
||||
ToolMessage or Command.
|
||||
@@ -915,14 +1039,14 @@ class ToolNode(RunnableCallable):
|
||||
|
||||
# Validate tool exists when we actually need to execute it
|
||||
if tool is None:
|
||||
if invalid_tool_message := self._validate_tool_call(call):
|
||||
if invalid_tool_message := self._validate_tool_call(call, tools_by_name):
|
||||
return invalid_tool_message
|
||||
# This should never happen if validation works correctly
|
||||
msg = f"Tool {call['name']} is not registered with ToolNode"
|
||||
raise TypeError(msg)
|
||||
|
||||
# Inject state, store, and runtime right before invocation
|
||||
injected_call = self._inject_tool_args(call, request.runtime)
|
||||
injected_call = self._inject_tool_args(call, request.runtime, tool)
|
||||
call_args = {**injected_call, "type": "tool_call"}
|
||||
|
||||
try:
|
||||
@@ -930,7 +1054,7 @@ class ToolNode(RunnableCallable):
|
||||
response = tool.invoke(call_args, config)
|
||||
except ValidationError as exc:
|
||||
# Filter out errors for injected arguments
|
||||
injected = self._injected_args.get(call["name"])
|
||||
injected = _get_all_injected_args(tool)
|
||||
filtered_errors = _filter_validation_errors(exc, injected)
|
||||
# Use original call["args"] without injected values for error reporting
|
||||
raise ToolInvocationError(
|
||||
@@ -994,6 +1118,7 @@ class ToolNode(RunnableCallable):
|
||||
call: ToolCall,
|
||||
input_type: Literal["list", "dict", "tool_calls"],
|
||||
tool_runtime: ToolRuntime,
|
||||
tools_by_name: dict[str, BaseTool],
|
||||
) -> ToolMessage | Command:
|
||||
"""Execute single tool call with wrap_tool_call wrapper if configured.
|
||||
|
||||
@@ -1001,13 +1126,14 @@ class ToolNode(RunnableCallable):
|
||||
call: Tool call dict.
|
||||
input_type: Input format.
|
||||
tool_runtime: Tool runtime.
|
||||
tools_by_name: Mapping from tool name to BaseTool.
|
||||
|
||||
Returns:
|
||||
ToolMessage or Command.
|
||||
"""
|
||||
# Validation is deferred to _execute_tool_sync to allow interceptors
|
||||
# to short-circuit requests for unregistered tools
|
||||
tool = self.tools_by_name.get(call["name"])
|
||||
tool = tools_by_name.get(call["name"])
|
||||
|
||||
# Create the tool request with state and runtime
|
||||
tool_request = ToolCallRequest(
|
||||
@@ -1021,12 +1147,14 @@ class ToolNode(RunnableCallable):
|
||||
|
||||
if self._wrap_tool_call is None:
|
||||
# No wrapper - execute directly
|
||||
return self._execute_tool_sync(tool_request, input_type, config)
|
||||
return self._execute_tool_sync(
|
||||
tool_request, input_type, config, tools_by_name
|
||||
)
|
||||
|
||||
# Define execute callable that can be called multiple times
|
||||
def execute(req: ToolCallRequest) -> ToolMessage | Command:
|
||||
"""Execute tool with given request. Can be called multiple times."""
|
||||
return self._execute_tool_sync(req, input_type, config)
|
||||
return self._execute_tool_sync(req, input_type, config, tools_by_name)
|
||||
|
||||
# Call wrapper with request and execute callable
|
||||
try:
|
||||
@@ -1049,13 +1177,17 @@ class ToolNode(RunnableCallable):
|
||||
request: ToolCallRequest,
|
||||
input_type: Literal["list", "dict", "tool_calls"],
|
||||
config: RunnableConfig,
|
||||
tools_by_name: dict[str, BaseTool],
|
||||
) -> ToolMessage | Command:
|
||||
"""Execute tool call asynchronously with configured error handling.
|
||||
|
||||
Args:
|
||||
request: Tool execution request.
|
||||
request: Tool execution request (includes the specific tool to execute).
|
||||
input_type: Input format.
|
||||
config: Runnable configuration.
|
||||
tools_by_name: Mapping from tool name to BaseTool. Used only for
|
||||
validation error messages when the requested tool doesn't exist,
|
||||
to list available tools in the error response.
|
||||
|
||||
Returns:
|
||||
ToolMessage or Command.
|
||||
@@ -1068,14 +1200,14 @@ class ToolNode(RunnableCallable):
|
||||
|
||||
# Validate tool exists when we actually need to execute it
|
||||
if tool is None:
|
||||
if invalid_tool_message := self._validate_tool_call(call):
|
||||
if invalid_tool_message := self._validate_tool_call(call, tools_by_name):
|
||||
return invalid_tool_message
|
||||
# This should never happen if validation works correctly
|
||||
msg = f"Tool {call['name']} is not registered with ToolNode"
|
||||
raise TypeError(msg)
|
||||
|
||||
# Inject state, store, and runtime right before invocation
|
||||
injected_call = self._inject_tool_args(call, request.runtime)
|
||||
injected_call = self._inject_tool_args(call, request.runtime, tool)
|
||||
call_args = {**injected_call, "type": "tool_call"}
|
||||
|
||||
try:
|
||||
@@ -1083,7 +1215,7 @@ class ToolNode(RunnableCallable):
|
||||
response = await tool.ainvoke(call_args, config)
|
||||
except ValidationError as exc:
|
||||
# Filter out errors for injected arguments
|
||||
injected = self._injected_args.get(call["name"])
|
||||
injected = _get_all_injected_args(tool)
|
||||
filtered_errors = _filter_validation_errors(exc, injected)
|
||||
# Use original call["args"] without injected values for error reporting
|
||||
raise ToolInvocationError(
|
||||
@@ -1147,6 +1279,7 @@ class ToolNode(RunnableCallable):
|
||||
call: ToolCall,
|
||||
input_type: Literal["list", "dict", "tool_calls"],
|
||||
tool_runtime: ToolRuntime,
|
||||
tools_by_name: dict[str, BaseTool],
|
||||
) -> ToolMessage | Command:
|
||||
"""Execute single tool call asynchronously with awrap_tool_call wrapper if configured.
|
||||
|
||||
@@ -1154,13 +1287,14 @@ class ToolNode(RunnableCallable):
|
||||
call: Tool call dict.
|
||||
input_type: Input format.
|
||||
tool_runtime: Tool runtime.
|
||||
tools_by_name: Mapping from tool name to BaseTool.
|
||||
|
||||
Returns:
|
||||
ToolMessage or Command.
|
||||
"""
|
||||
# Validation is deferred to _execute_tool_async to allow interceptors
|
||||
# to short-circuit requests for unregistered tools
|
||||
tool = self.tools_by_name.get(call["name"])
|
||||
tool = tools_by_name.get(call["name"])
|
||||
|
||||
# Create the tool request with state and runtime
|
||||
tool_request = ToolCallRequest(
|
||||
@@ -1174,16 +1308,20 @@ class ToolNode(RunnableCallable):
|
||||
|
||||
if self._awrap_tool_call is None and self._wrap_tool_call is None:
|
||||
# No wrapper - execute directly
|
||||
return await self._execute_tool_async(tool_request, input_type, config)
|
||||
return await self._execute_tool_async(
|
||||
tool_request, input_type, config, tools_by_name
|
||||
)
|
||||
|
||||
# Define async execute callable that can be called multiple times
|
||||
async def execute(req: ToolCallRequest) -> ToolMessage | Command:
|
||||
"""Execute tool with given request. Can be called multiple times."""
|
||||
return await self._execute_tool_async(req, input_type, config)
|
||||
return await self._execute_tool_async(
|
||||
req, input_type, config, tools_by_name
|
||||
)
|
||||
|
||||
def _sync_execute(req: ToolCallRequest) -> ToolMessage | Command:
|
||||
"""Sync execute fallback for sync wrapper."""
|
||||
return self._execute_tool_sync(req, input_type, config)
|
||||
return self._execute_tool_sync(req, input_type, config, tools_by_name)
|
||||
|
||||
# Call wrapper with request and execute callable
|
||||
try:
|
||||
@@ -1249,10 +1387,12 @@ class ToolNode(RunnableCallable):
|
||||
tool_calls = list(latest_ai_message.tool_calls)
|
||||
return tool_calls, input_type
|
||||
|
||||
def _validate_tool_call(self, call: ToolCall) -> ToolMessage | None:
|
||||
def _validate_tool_call(
|
||||
self, call: ToolCall, tools_by_name: dict[str, BaseTool]
|
||||
) -> ToolMessage | None:
|
||||
requested_tool = call["name"]
|
||||
if requested_tool not in self.tools_by_name:
|
||||
all_tool_names = list(self.tools_by_name.keys())
|
||||
if requested_tool not in tools_by_name:
|
||||
all_tool_names = list(tools_by_name.keys())
|
||||
content = INVALID_TOOL_NAME_ERROR_TEMPLATE.format(
|
||||
requested_tool=requested_tool,
|
||||
available_tools=", ".join(all_tool_names),
|
||||
@@ -1281,6 +1421,7 @@ class ToolNode(RunnableCallable):
|
||||
self,
|
||||
tool_call: ToolCall,
|
||||
tool_runtime: ToolRuntime,
|
||||
tool: BaseTool,
|
||||
) -> ToolCall:
|
||||
"""Inject graph state, store, and runtime into tool call arguments.
|
||||
|
||||
@@ -1299,6 +1440,7 @@ class ToolNode(RunnableCallable):
|
||||
Must contain 'name', 'args', 'id', and 'type' fields.
|
||||
tool_runtime: The ToolRuntime instance containing all runtime context
|
||||
(state, config, store, context, stream_writer) to inject into tools.
|
||||
tool: The BaseTool instance to inject arguments for.
|
||||
|
||||
Returns:
|
||||
A new ToolCall dictionary with the same structure as the input but with
|
||||
@@ -1312,11 +1454,8 @@ class ToolNode(RunnableCallable):
|
||||
This method is called automatically during tool execution. It should not
|
||||
be called from outside the `ToolNode`.
|
||||
"""
|
||||
if tool_call["name"] not in self.tools_by_name:
|
||||
return tool_call
|
||||
|
||||
injected = self._injected_args.get(tool_call["name"])
|
||||
if not injected:
|
||||
injected = _get_all_injected_args(tool)
|
||||
if not injected.state and not injected.store and not injected.runtime:
|
||||
return tool_call
|
||||
|
||||
tool_call_copy: ToolCall = copy(tool_call)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.7"
|
||||
version = "1.0.6"
|
||||
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -1902,3 +1902,356 @@ async def test_tool_node_tool_runtime_generic() -> None:
|
||||
assert tool_message.type == "tool"
|
||||
assert tool_message.content == "test_info"
|
||||
assert tool_message.tool_call_id == "call_1"
|
||||
|
||||
|
||||
async def test_tool_node_dynamic_tools() -> None:
|
||||
"""Test ToolNode with a dynamic tools provider callable."""
|
||||
|
||||
@dec_tool
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
@dec_tool
|
||||
def multiply(a: int, b: int) -> int:
|
||||
"""Multiply two numbers."""
|
||||
return a * b
|
||||
|
||||
@dec_tool
|
||||
def subtract(a: int, b: int) -> int:
|
||||
"""Subtract two numbers."""
|
||||
return a - b
|
||||
|
||||
# Track which tools are available
|
||||
available_tools: list[BaseTool] = [add, multiply]
|
||||
|
||||
def get_tools() -> list[BaseTool]:
|
||||
return available_tools
|
||||
|
||||
# Create ToolNode with dynamic tools provider
|
||||
tool_node = ToolNode(get_tools)
|
||||
|
||||
# Test that tools_by_name returns the current tools
|
||||
assert set(tool_node.tools_by_name.keys()) == {"add", "multiply"}
|
||||
|
||||
# Test invoking a tool
|
||||
result = tool_node.invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"test",
|
||||
tool_calls=[
|
||||
{"name": "add", "args": {"a": 2, "b": 3}, "id": "call_1"}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "5"
|
||||
|
||||
# Test invoking another tool
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"test",
|
||||
tool_calls=[
|
||||
{"name": "multiply", "args": {"a": 4, "b": 5}, "id": "call_2"}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "20"
|
||||
|
||||
# Change the available tools dynamically
|
||||
available_tools.clear()
|
||||
available_tools.extend([subtract])
|
||||
|
||||
# Verify tools_by_name reflects the change
|
||||
assert set(tool_node.tools_by_name.keys()) == {"subtract"}
|
||||
|
||||
# Test that the old tool is no longer available
|
||||
result = tool_node.invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"test",
|
||||
tool_calls=[
|
||||
{"name": "add", "args": {"a": 2, "b": 3}, "id": "call_3"}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.status == "error"
|
||||
assert "add is not a valid tool" in tool_message.content
|
||||
|
||||
# Test that the new tool works
|
||||
result = tool_node.invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"test",
|
||||
tool_calls=[
|
||||
{"name": "subtract", "args": {"a": 10, "b": 3}, "id": "call_4"}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "7"
|
||||
|
||||
|
||||
async def test_tool_node_dynamic_tools_with_injection() -> None:
|
||||
"""Test dynamic tools with state injection."""
|
||||
|
||||
class TestState(TypedDict):
|
||||
messages: list
|
||||
multiplier: int
|
||||
|
||||
@dec_tool
|
||||
def scale(
|
||||
value: int,
|
||||
multiplier: Annotated[int, InjectedState("multiplier")],
|
||||
) -> int:
|
||||
"""Scale a value by the multiplier from state."""
|
||||
return value * multiplier
|
||||
|
||||
available_tools: list[BaseTool] = [scale]
|
||||
|
||||
def get_tools() -> list[BaseTool]:
|
||||
return available_tools
|
||||
|
||||
tool_node = ToolNode(get_tools)
|
||||
|
||||
result = tool_node.invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"test",
|
||||
tool_calls=[
|
||||
{"name": "scale", "args": {"value": 5}, "id": "call_1"}
|
||||
],
|
||||
)
|
||||
],
|
||||
"multiplier": 3,
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "15"
|
||||
|
||||
|
||||
def test_tool_node_dynamic_tools_type_error() -> None:
|
||||
"""Test that dynamic tools provider must return BaseTool instances."""
|
||||
|
||||
def bad_tool_provider():
|
||||
# Returns a plain function instead of BaseTool
|
||||
def not_a_base_tool(x: int) -> int:
|
||||
return x
|
||||
|
||||
return [not_a_base_tool]
|
||||
|
||||
tool_node = ToolNode(bad_tool_provider)
|
||||
|
||||
# Should raise TypeError when trying to invoke since the provider returns
|
||||
# a function instead of BaseTool
|
||||
with pytest.raises(TypeError, match="must return BaseTool instances"):
|
||||
tool_node.invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"test",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "not_a_base_tool",
|
||||
"args": {"x": 1},
|
||||
"id": "call_1",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
|
||||
async def test_tool_node_async_tools_provider() -> None:
|
||||
"""Test ToolNode with an async tools provider callable."""
|
||||
|
||||
@dec_tool
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
@dec_tool
|
||||
def multiply(a: int, b: int) -> int:
|
||||
"""Multiply two numbers."""
|
||||
return a * b
|
||||
|
||||
@dec_tool
|
||||
def subtract(a: int, b: int) -> int:
|
||||
"""Subtract two numbers."""
|
||||
return a - b
|
||||
|
||||
# Track which tools are available
|
||||
available_tools: list[BaseTool] = [add, multiply]
|
||||
|
||||
async def get_tools_async() -> list[BaseTool]:
|
||||
# Simulate async operation (e.g., fetching tools from a database)
|
||||
return available_tools
|
||||
|
||||
# Create ToolNode with async dynamic tools provider
|
||||
tool_node = ToolNode(get_tools_async)
|
||||
|
||||
# Test invoking a tool asynchronously
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"test",
|
||||
tool_calls=[
|
||||
{"name": "add", "args": {"a": 2, "b": 3}, "id": "call_1"}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "5"
|
||||
|
||||
# Test invoking another tool
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"test",
|
||||
tool_calls=[
|
||||
{"name": "multiply", "args": {"a": 4, "b": 5}, "id": "call_2"}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "20"
|
||||
|
||||
# Change the available tools dynamically
|
||||
available_tools.clear()
|
||||
available_tools.extend([subtract])
|
||||
|
||||
# Test that the new tool works
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"test",
|
||||
tool_calls=[
|
||||
{"name": "subtract", "args": {"a": 10, "b": 3}, "id": "call_4"}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "7"
|
||||
|
||||
|
||||
def test_tool_node_async_tools_provider_sync_context_error() -> None:
|
||||
"""Test that async tools provider raises TypeError in sync context."""
|
||||
|
||||
@dec_tool
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
async def get_tools_async() -> list[BaseTool]:
|
||||
return [add]
|
||||
|
||||
tool_node = ToolNode(get_tools_async)
|
||||
|
||||
# Should raise TypeError when trying to invoke synchronously
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match="Cannot use async tools provider in synchronous context",
|
||||
):
|
||||
tool_node.invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"test",
|
||||
tool_calls=[
|
||||
{"name": "add", "args": {"a": 2, "b": 3}, "id": "call_1"}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
|
||||
def test_tool_node_async_tools_provider_tools_by_name_error() -> None:
|
||||
"""Test that tools_by_name raises TypeError with async tools provider."""
|
||||
|
||||
@dec_tool
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
async def get_tools_async() -> list[BaseTool]:
|
||||
return [add]
|
||||
|
||||
tool_node = ToolNode(get_tools_async)
|
||||
|
||||
# Should raise TypeError when accessing tools_by_name
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match="Cannot use async tools provider in synchronous context",
|
||||
):
|
||||
_ = tool_node.tools_by_name
|
||||
|
||||
|
||||
async def test_tool_node_async_tools_provider_type_error() -> None:
|
||||
"""Test that async tools provider must return BaseTool instances."""
|
||||
|
||||
async def bad_tool_provider():
|
||||
# Returns a plain function instead of BaseTool
|
||||
def not_a_base_tool(x: int) -> int:
|
||||
return x
|
||||
|
||||
return [not_a_base_tool]
|
||||
|
||||
tool_node = ToolNode(bad_tool_provider)
|
||||
|
||||
# Should raise TypeError when trying to invoke since the provider returns
|
||||
# a function instead of BaseTool
|
||||
with pytest.raises(TypeError, match="must return BaseTool instances"):
|
||||
await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"test",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "not_a_base_tool",
|
||||
"args": {"x": 1},
|
||||
"id": "call_1",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
@@ -220,10 +220,7 @@ def test_unregistered_tool_error_when_interceptor_calls_execute() -> None:
|
||||
)
|
||||
# Should get validation error message
|
||||
assert result[0].status == "error"
|
||||
assert (
|
||||
result[0].content
|
||||
== "Error: unregistered_tool is not a valid tool, try one of [registered_tool]."
|
||||
)
|
||||
assert "is not a valid tool" in result[0].content
|
||||
assert result[0].tool_call_id == "2"
|
||||
|
||||
|
||||
@@ -579,226 +576,3 @@ def test_interceptor_verifies_tool_is_none_for_unregistered() -> None:
|
||||
assert len(captured_requests) == 1
|
||||
assert captured_requests[0].tool is not None
|
||||
assert captured_requests[0].tool.name == "registered_tool"
|
||||
|
||||
|
||||
def test_wrap_tool_call_override_unregistered_tool_with_custom_impl() -> None:
|
||||
"""Test that wrap_tool_call can provide custom implementation for unregistered tool."""
|
||||
called = False
|
||||
|
||||
@dec_tool
|
||||
def custom_tool_impl() -> str:
|
||||
"""Custom tool implementation."""
|
||||
nonlocal called
|
||||
called = True
|
||||
return "custom result"
|
||||
|
||||
def hook(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
if request.tool_call["name"] == "custom_tool":
|
||||
assert request.tool is None # Unregistered tools have tool=None
|
||||
return execute(request.override(tool=custom_tool_impl))
|
||||
return execute(request)
|
||||
|
||||
node = ToolNode([registered_tool], wrap_tool_call=hook)
|
||||
|
||||
result = node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{"name": "custom_tool", "args": {}, "id": "1", "type": "tool_call"}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
assert called
|
||||
assert result[0].content == "custom result"
|
||||
assert result[0].tool_call_id == "1"
|
||||
|
||||
|
||||
async def test_awrap_tool_call_override_unregistered_tool_with_custom_impl() -> None:
|
||||
"""Test that awrap_tool_call can provide custom implementation for unregistered tool."""
|
||||
called = False
|
||||
|
||||
@dec_tool
|
||||
def custom_async_tool_impl() -> str:
|
||||
"""Custom async tool implementation."""
|
||||
nonlocal called
|
||||
called = True
|
||||
return "async custom result"
|
||||
|
||||
async def hook(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
||||
) -> ToolMessage | Command:
|
||||
if request.tool_call["name"] == "custom_async_tool":
|
||||
assert request.tool is None # Unregistered tools have tool=None
|
||||
return await execute(request.override(tool=custom_async_tool_impl))
|
||||
return await execute(request)
|
||||
|
||||
node = ToolNode([registered_tool], awrap_tool_call=hook)
|
||||
|
||||
result = await node.ainvoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "custom_async_tool",
|
||||
"args": {},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
assert called
|
||||
assert result[0].content == "async custom result"
|
||||
assert result[0].tool_call_id == "1"
|
||||
|
||||
|
||||
def test_graceful_failure_when_hook_does_not_override_unregistered_tool_sync() -> None:
|
||||
"""Test graceful failure when hook doesn't override unregistered tool."""
|
||||
|
||||
def passthrough_hook(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
return execute(request)
|
||||
|
||||
node = ToolNode(
|
||||
[registered_tool],
|
||||
wrap_tool_call=passthrough_hook,
|
||||
handle_tool_errors=True,
|
||||
)
|
||||
|
||||
result = node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{"name": "nonexistent", "args": {}, "id": "1", "type": "tool_call"}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
assert result[0].status == "error"
|
||||
assert result[0].tool_call_id == "1"
|
||||
assert (
|
||||
result[0].content
|
||||
== "Error: nonexistent is not a valid tool, try one of [registered_tool]."
|
||||
)
|
||||
|
||||
|
||||
def test_graceful_failure_even_when_handle_errors_disabled_sync() -> None:
|
||||
"""Test that unregistered tool validation returns error even with handle_tool_errors=False."""
|
||||
|
||||
def passthrough_hook(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
return execute(request)
|
||||
|
||||
node = ToolNode(
|
||||
[registered_tool],
|
||||
wrap_tool_call=passthrough_hook,
|
||||
handle_tool_errors=False,
|
||||
)
|
||||
|
||||
result = node.invoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{"name": "missing", "args": {}, "id": "1", "type": "tool_call"}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
assert result[0].status == "error"
|
||||
assert (
|
||||
result[0].content
|
||||
== "Error: missing is not a valid tool, try one of [registered_tool]."
|
||||
)
|
||||
|
||||
|
||||
async def test_graceful_failure_when_hook_does_not_override_unregistered_tool_async() -> (
|
||||
None
|
||||
):
|
||||
"""Test graceful failure when async hook doesn't override unregistered tool."""
|
||||
|
||||
async def passthrough_hook(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
||||
) -> ToolMessage | Command:
|
||||
return await execute(request)
|
||||
|
||||
node = ToolNode(
|
||||
[registered_tool],
|
||||
awrap_tool_call=passthrough_hook,
|
||||
handle_tool_errors=True,
|
||||
)
|
||||
|
||||
result = await node.ainvoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{"name": "unknown", "args": {}, "id": "1", "type": "tool_call"}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
assert result[0].status == "error"
|
||||
assert result[0].tool_call_id == "1"
|
||||
assert (
|
||||
result[0].content
|
||||
== "Error: unknown is not a valid tool, try one of [registered_tool]."
|
||||
)
|
||||
|
||||
|
||||
async def test_graceful_failure_even_when_handle_errors_disabled_async() -> None:
|
||||
"""Test that async unregistered tool validation returns error even with handle_tool_errors=False."""
|
||||
|
||||
async def passthrough_hook(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
||||
) -> ToolMessage | Command:
|
||||
return await execute(request)
|
||||
|
||||
node = ToolNode(
|
||||
[registered_tool],
|
||||
awrap_tool_call=passthrough_hook,
|
||||
handle_tool_errors=False,
|
||||
)
|
||||
|
||||
result = await node.ainvoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{"name": "missing", "args": {}, "id": "1", "type": "tool_call"}
|
||||
],
|
||||
)
|
||||
],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
assert result[0].status == "error"
|
||||
assert (
|
||||
result[0].content
|
||||
== "Error: missing is not a valid tool, try one of [registered_tool]."
|
||||
)
|
||||
|
||||
Generated
+2
-2
@@ -268,7 +268,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.7"
|
||||
version = "1.0.6"
|
||||
source = { editable = "../langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -489,7 +489,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.7"
|
||||
version = "1.0.6"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
|
||||
Reference in New Issue
Block a user