Compare commits

..
16 Commits
Author SHA1 Message Date
David DuongandGitHub cddcf35c09 fix(cli): invert assumed python_version / js_version check (#4245) 2025-04-11 16:49:46 +02:00
Tat Dat Duong 5eefc1d55d fix(cli): invert assumed python_version / js_version check 2025-04-11 16:38:26 +02:00
Vadym BardaandGitHub c9d4f1d77d langgraph: release 0.3.28 (#4243) 2025-04-10 21:17:25 -04:00
Vadym BardaandGitHub 1e2888ce39 langgraph: allow passing a list of retry policies (#4240)
* support passing `retry=(RetryPolicy(...), RetryPolicy())`
* fix bugs with `default_retry_on` and backoff calculation
* add tests
2025-04-10 21:16:27 -04:00
David DuongandGitHub 2a7d48582f release(cli): 0.2.2 (#4241) 2025-04-11 01:40:35 +02:00
Tat Dat Duong b7bd87a063 release(cli): 0.2.2 2025-04-11 01:34:08 +02:00
David DuongandGitHub 2a825cc0e0 feat(cli): add multiplatform support (#4239)
- Uses new `install-node.sh` script already used for Python Gen UI
- Add default `node_version` / `python_version` based on provided
`graphs`

Closes #4115
2025-04-11 01:32:27 +02:00
Tat Dat Duong 436902e5a3 Consolidate node_version and python_version fix 2025-04-11 01:04:26 +02:00
Tat Dat Duong 0ac6a96c6e Fix up 2025-04-11 00:41:16 +02:00
Tat Dat Duong d9856d92af Another 3.9 fix 2025-04-11 00:09:34 +02:00
Tat Dat Duong b3487cbc49 Fix Python 3.9 2025-04-11 00:06:31 +02:00
Tat Dat Duong d06075cbcf Fix new style config 2025-04-11 00:04:44 +02:00
Tat Dat Duong e0be9ae2ef feat(cli): add multiplatform support
Uses new `install-node.sh` script already used for Python Gen UI, add default `node_version` / `python_version` based on provided `graphs`
2025-04-10 23:52:49 +02:00
Vadym BardaandGitHub 19cfe3a0a9 docs: fix title (#4238) 2025-04-10 17:17:36 -04:00
David DuongandGitHub 99a87abaa5 feat(docs): update typedoc references for auth (#4236) 2025-04-10 21:29:19 +02:00
Tat Dat Duong 4be86b2a51 feat(docs): update typedoc references for auth 2025-04-10 17:03:03 +02:00
25 changed files with 764 additions and 210 deletions
+1
View File
@@ -419,6 +419,7 @@ Here are all the supported action handlers:
| | `@auth.on.crons.search` | Listing cron jobs | [`CronsSearch`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.CronsSearch) |
???+ note "About Runs"
Runs are scoped to their parent thread for access control. This means permissions are typically inherited from the thread, reflecting the conversational nature of the data model. All run operations (reading, listing) except creation are controlled by the thread's handlers.
There is a specific `create_run` handler for creating new runs because it had more arguments that you can view in the handler.
-4
View File
@@ -9,10 +9,6 @@
For a more guided walkthrough, see [**setting up custom authentication**](../../tutorials/auth/getting_started.md) tutorial.
???+ note "Python only"
We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon.
???+ note "Support by deployment type"
Custom auth is supported for all deployments in the **managed LangGraph Cloud**, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
+4 -1
View File
@@ -1,6 +1,6 @@
---
hide_comments: true
title: Home
title: LangGraph
---
<script>
@@ -23,6 +23,9 @@ title: Home
.md-content h1 {
display: none;
}
.md-header__topic {
display: none;
}
</style>
{!../README.md!}
+4 -26
View File
@@ -274,23 +274,13 @@ def _build(
tag: str,
passthrough: Sequence[str] = (),
):
base_image = base_image or (
"langchain/langgraphjs-api"
if config_json.get("node_version")
else "langchain/langgraph-api"
)
# pull latest images
if pull:
runner.run(
subp_exec(
"docker",
"pull",
(
f"{base_image}:{config_json['node_version']}"
if config_json.get("node_version")
else f"{base_image}:{config_json['python_version']}"
),
langgraph_cli.config.docker_tag(config_json, base_image),
verbose=True,
)
)
@@ -450,11 +440,7 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -
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"
),
None,
)
with open(str(save_path), "w", encoding="utf-8") as f:
f.write(dockerfile)
@@ -719,11 +705,7 @@ def prepare_args_and_stdin(
config_path,
config,
watch=watch,
base_image=(
"langchain/langgraphjs-api"
if config.get("node_version")
else "langchain/langgraph-api"
),
base_image=langgraph_cli.config.default_base_image(config),
)
return args, stdin
@@ -750,11 +732,7 @@ def prepare(
subp_exec(
"docker",
"pull",
(
f"langchain/langgraphjs-api:{config_json['node_version']}"
if config_json.get("node_version")
else f"langchain/langgraph-api:{config_json['python_version']}"
),
langgraph_cli.config.docker_tag(config_json),
verbose=verbose,
)
)
+126 -85
View File
@@ -8,7 +8,10 @@ from typing import Any, Literal, NamedTuple, Optional, TypedDict, Union
import click
MIN_NODE_VERSION = "20"
DEFAULT_NODE_VERSION = "20"
MIN_PYTHON_VERSION = "3.11"
DEFAULT_PYTHON_VERSION = "3.11"
class TTLConfig(TypedDict, total=False):
@@ -440,38 +443,54 @@ def _parse_node_version(version_str: str) -> int:
) from None
def _is_node_graph(spec: Union[str, dict]) -> bool:
"""Check if a graph is a Node.js graph based on the file extension."""
if isinstance(spec, dict):
spec = spec.get("path")
file_path = spec.split(":")[0]
file_ext = os.path.splitext(file_path)[1]
return file_ext in [
".ts",
".mts",
".cts",
".js",
".mjs",
".cjs",
]
def validate_config(config: Config) -> Config:
"""Validate a configuration dictionary."""
config = (
{
"node_version": config.get("node_version"),
"dockerfile_lines": config.get("dockerfile_lines", []),
"dependencies": config.get("dependencies", []),
"graphs": config.get("graphs", {}),
"env": config.get("env", {}),
"store": config.get("store"),
"auth": config.get("auth"),
"http": config.get("http"),
"checkpointer": config.get("checkpointer"),
"ui": config.get("ui"),
"ui_config": config.get("ui_config"),
}
if config.get("node_version")
else {
"python_version": config.get("python_version", "3.11"),
"pip_config_file": config.get("pip_config_file"),
"dockerfile_lines": config.get("dockerfile_lines", []),
"dependencies": config.get("dependencies", []),
"graphs": config.get("graphs", {}),
"env": config.get("env", {}),
"store": config.get("store"),
"auth": config.get("auth"),
"http": config.get("http"),
"checkpointer": config.get("checkpointer"),
"ui": config.get("ui"),
"ui_config": config.get("ui_config"),
}
graphs = config.get("graphs", {})
some_node = any(_is_node_graph(spec) for spec in graphs.values())
some_python = any(not _is_node_graph(spec) for spec in graphs.values())
node_version = config.get(
"node_version", DEFAULT_NODE_VERSION if some_node else None
)
python_version = config.get(
"python_version", DEFAULT_PYTHON_VERSION if some_python else None
)
config = {
"node_version": node_version,
"python_version": python_version,
"pip_config_file": config.get("pip_config_file"),
"dependencies": config.get("dependencies", []),
"dockerfile_lines": config.get("dockerfile_lines", []),
"graphs": config.get("graphs", {}),
"env": config.get("env", {}),
"store": config.get("store"),
"auth": config.get("auth"),
"http": config.get("http"),
"checkpointer": config.get("checkpointer"),
"ui": config.get("ui"),
"ui_config": config.get("ui_config"),
}
if config.get("node_version"):
node_version = config["node_version"]
@@ -1085,26 +1104,11 @@ ADD {relpath} /deps/{name}
for fullpath, (relpath, name) in local_deps.real_pkgs.items()
)
ui_inst_str: str = ""
install_node_str: str = ""
if config.get("ui") and local_deps.working_dir:
install_node_str = "RUN /storage/install-node.sh"
ui_inst: list[str] = []
ui_inst.append(f"ENV LANGGRAPH_UI='{json.dumps(config['ui'])}'")
if config.get("ui_config"):
ui_inst.append(
f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(config['ui_config'])}'"
)
ui_inst.append(
f"RUN cd {local_deps.working_dir} && {_get_node_pm_install_cmd(config_path, config)} && tsx /api/langgraph_api/js/build.mts",
)
ui_inst_str = f"""# -- Installing UI dependencies --
{os.linesep.join(ui_inst)}
# -- End of UI dependencies install --"""
install_node_str: str = (
"RUN /storage/install-node.sh"
if (config.get("ui") or config.get("node_version")) and local_deps.working_dir
else ""
)
installs = f"{os.linesep}{os.linesep}".join(
filter(
@@ -1136,8 +1140,24 @@ ADD {relpath} /deps/{name}
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
)
graphs = config["graphs"]
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(graphs)}'")
if (ui := config.get("ui")) is not None:
env_vars.append(f"ENV LANGGRAPH_UI='{json.dumps(ui)}'")
if (ui_config := config.get("ui_config")) is not None:
env_vars.append(f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(ui_config)}'")
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'")
js_inst_str: str = ""
if (config.get("ui") or config.get("node_version")) and local_deps.working_dir:
js_inst_str = os.linesep.join(
[
"# -- Installing JS dependencies --",
f"ENV NODE_VERSION={config.get('node_version') or DEFAULT_NODE_VERSION}",
f"RUN cd {local_deps.working_dir} && {_get_node_pm_install_cmd(config_path, config)} && tsx /api/langgraph_api/js/build.mts",
"# -- End of JS dependencies install --",
]
)
docker_file_contents = [
f"FROM {base_image}:{config['python_version']}",
@@ -1151,7 +1171,7 @@ ADD {relpath} /deps/{name}
"# -- End of local dependencies install --",
os.linesep.join(env_vars),
"",
ui_inst_str,
js_inst_str,
"",
PIP_CLEANUP_LINES, # Add pip cleanup after all installations are complete
"",
@@ -1176,51 +1196,70 @@ def node_config_to_docker(
) -> tuple[str, dict[str, str]]:
faux_path = f"/deps/{config_path.parent.name}"
install_cmd = _get_node_pm_install_cmd(config_path, config)
store_config = config.get("store")
env_additional_config = (
""
if not store_config
else f"""
ENV LANGGRAPH_STORE='{json.dumps(store_config)}'
"""
)
env_vars: list[str] = []
if (store_config := config.get("store")) is not None:
env_vars.append(f"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)}'
"""
env_vars.append(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)}'
"""
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
if (checkpointer_config := config.get("checkpointer")) is not None:
env_additional_config += f"""
ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'
"""
env_vars.append(
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
)
return (
f"""FROM {base_image}:{config['node_version']}
if ui := config.get("ui"):
env_vars.append(f"ENV LANGGRAPH_UI='{json.dumps(ui)}'")
{os.linesep.join(config["dockerfile_lines"])}
if ui_config := config.get("ui_config"):
env_vars.append(f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(ui_config)}'")
ADD . {faux_path}
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'")
RUN cd {faux_path} && {install_cmd}
{env_additional_config}
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
{f"ENV LANGGRAPH_UI='{json.dumps(config['ui'])}'" if config.get("ui") else ""}
{f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(config['ui_config'])}'" if config.get("ui_config") else ""}
docker_file_contents = [
f"FROM {base_image}:{config['node_version']}",
"",
os.linesep.join(config["dockerfile_lines"]),
"",
f"ADD . {faux_path}",
"",
f"RUN cd {faux_path} && {install_cmd}",
"",
os.linesep.join(env_vars),
"",
f"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',
]
WORKDIR {faux_path}
return os.linesep.join(docker_file_contents), {}
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts""",
{},
)
def default_base_image(config: Config) -> str:
if config.get("node_version") and not config.get("python_version"):
return "langchain/langgraphjs-api"
return "langchain/langgraph-api"
def docker_tag(config: Config, base_image: Optional[str] = None) -> str:
base_image = base_image or default_base_image(config)
if config.get("node_version") and not config.get("python_version"):
return f"{base_image}:{config['node_version']}"
return f"{base_image}:{config['python_version']}"
def config_to_docker(
config_path: pathlib.Path, config: Config, base_image: str
config_path: pathlib.Path, config: Config, base_image: Optional[str] = None
) -> tuple[str, dict[str, str]]:
if config.get("node_version"):
base_image = base_image or default_base_image(config)
if config.get("node_version") and not config.get("python_version"):
return node_config_to_docker(config_path, config, base_image)
return python_config_to_docker(config_path, config, base_image)
@@ -1229,9 +1268,11 @@ def config_to_docker(
def config_to_compose(
config_path: pathlib.Path,
config: Config,
base_image: str,
base_image: Optional[str] = None,
watch: bool = False,
) -> str:
base_image = base_image or default_base_image(config)
env_vars = config["env"].items() if isinstance(config["env"], dict) else {}
env_vars_str = "\n".join(f' {k}: "{v}"' for k, v in env_vars)
env_file_str = (
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.2.1"
version = "0.2.3"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
+2 -1
View File
@@ -178,8 +178,9 @@ def test_dockerfile_command_basic() -> None:
"""Test the 'dockerfile' command with basic configuration."""
runner = CliRunner()
config_content = {
"node_version": "20", # Add any other necessary configuration fields
"python_version": "3.11",
"graphs": {"agent": "agent.py:graph"},
"dependencies": ["."],
}
with temporary_config_folder(config_content) as temp_dir:
+103 -11
View File
@@ -27,8 +27,10 @@ def test_validate_config():
"agent": "./agent.py:graph",
},
}
actual_config = validate_config(expected_config)
expected_config = {
"python_version": "3.11",
"node_version": None,
"pip_config_file": None,
"dockerfile_lines": [],
"env": {},
@@ -40,13 +42,13 @@ def test_validate_config():
"ui_config": None,
**expected_config,
}
actual_config = validate_config(expected_config)
assert actual_config == expected_config
# full config
env = ".env"
expected_config = {
"python_version": "3.12",
"node_version": None,
"pip_config_file": "pipconfig.txt",
"dockerfile_lines": ["ARG meow"],
"dependencies": [".", "langchain"],
@@ -69,16 +71,12 @@ def test_validate_config():
# check wrong python version raises
with pytest.raises(click.UsageError):
validate_config(
{
"python_version": "3.9",
}
)
validate_config({"python_version": "3.9"})
# check missing dependencies key raises
with pytest.raises(click.UsageError):
validate_config(
{"python_version": "3.9", "graphs": {"agent": "./agent.py:graph"}},
{"python_version": "3.9", "graphs": {"agent": "./agent.py:graph"}}
)
# check missing graphs key raises
@@ -196,6 +194,58 @@ def test_validate_config_file():
validate_config_file(config_path)
def test_validate_config_multiplatform():
# default node
config = validate_config(
{"dependencies": ["."], "graphs": {"js": "./js.mts:graph"}}
)
assert config["node_version"] == "20"
assert config["python_version"] is None
# default multiplatform
config = validate_config(
{
"node_version": "22",
"python_version": "3.12",
"dependencies": ["."],
"graphs": {"python": "./python.py:graph", "js": "./js.mts:graph"},
}
)
assert config["node_version"] == "22"
assert config["python_version"] == "3.12"
# default multiplatform (full infer)
graphs = {"python": "./python.py:graph", "js": "./js.mts:graph"}
config = validate_config({"dependencies": ["."], "graphs": graphs})
assert config["node_version"] == "20"
assert config["python_version"] == "3.11"
# default multiplatform (partial node)
config = validate_config(
{"node_version": "22", "dependencies": ["."], "graphs": graphs}
)
assert config["node_version"] == "22"
assert config["python_version"] == "3.11"
# default multiplatform (partial python)
config = validate_config(
{"python_version": "3.12", "dependencies": ["."], "graphs": graphs}
)
assert config["node_version"] == "20"
assert config["python_version"] == "3.12"
# no known extension (assumes python)
config = validate_config(
{
"dependencies": ["./local", "./shared_utils"],
"graphs": {"agent": "local.workflow:graph"},
"env": ".env",
}
)
assert config["node_version"] is None
assert config["python_version"] == "3.11"
# config_to_docker
def test_config_to_docker_simple():
graphs = {"agent": "./agent.py:graph"}
@@ -507,9 +557,9 @@ ARG foo
ADD . /deps/unit_tests
RUN cd /deps/unit_tests && npm i
ENV LANGGRAPH_AUTH='{"path": "./graphs/auth.mts:auth"}'
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}'
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
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"""
@@ -548,12 +598,54 @@ RUN set -ex && \\
# -- 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"}}'
# -- Installing UI dependencies --
ENV LANGGRAPH_UI='{{"agent": "./graphs/agent.ui.jsx"}}'
ENV LANGGRAPH_UI_CONFIG='{{"shared": ["nuqs"]}}'
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
# -- Installing JS dependencies --
ENV NODE_VERSION=20
RUN cd /deps/__outer_unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
# -- End of UI dependencies install --
# -- End of JS dependencies install --
{PIP_CLEANUP_LINES}
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_multiplatform():
graphs = {
"python": "./multiplatform/python.py:graph",
"js": "./multiplatform/js.mts:graph",
}
actual_docker_stdin, additional_contexts = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{"node_version": "22", "dependencies": ["."], "graphs": graphs}
),
"langchain/langgraph-api",
)
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.11
RUN /storage/install-node.sh
# -- 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 --
# -- 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='{{"python": "/deps/__outer_unit_tests/unit_tests/multiplatform/python.py:graph", "js": "/deps/__outer_unit_tests/unit_tests/multiplatform/js.mts:graph"}}'
# -- Installing JS dependencies --
ENV NODE_VERSION=22
RUN cd /deps/__outer_unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
# -- End of JS dependencies install --
{PIP_CLEANUP_LINES}
WORKDIR /deps/__outer_unit_tests/unit_tests"""
+8 -3
View File
@@ -9,6 +9,7 @@ from typing import (
Callable,
Generic,
Optional,
Sequence,
TypeVar,
Union,
get_args,
@@ -38,7 +39,7 @@ from langgraph.types import _DC_KWARGS, RetryPolicy, StreamMode
def task(
*,
name: Optional[str] = None,
retry: Optional[RetryPolicy] = None,
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
) -> Callable[
[Union[Callable[P, Awaitable[T]], Callable[P, T]]],
Callable[P, SyncAsyncFuture[T]],
@@ -55,7 +56,7 @@ def task(
__func_or_none__: Optional[Union[Callable[P, Awaitable[T]], Callable[P, T]]] = None,
*,
name: Optional[str] = None,
retry: Optional[RetryPolicy] = None,
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
) -> Union[
Callable[
[Union[Callable[P, Awaitable[T]], Callable[P, T]]],
@@ -119,6 +120,10 @@ def task(
await add_one.ainvoke([1, 2, 3]) # Returns [2, 3, 4]
```
"""
if isinstance(retry, RetryPolicy):
retry_policies: Optional[Sequence[RetryPolicy]] = (retry,)
else:
retry_policies = retry
def decorator(
func: Union[Callable[P, Awaitable[T]], Callable[P, T]],
@@ -137,7 +142,7 @@ def task(
# handle regular functions / partials / callable classes, etc.
func.__name__ = name
call_func = functools.partial(call, func, retry=retry)
call_func = functools.partial(call, func, retry=retry_policies)
object.__setattr__(call_func, "_is_pregel_task", True)
return functools.update_wrapper(call_func, func)
+6 -5
View File
@@ -109,7 +109,7 @@ class StateNodeSpec(NamedTuple):
runnable: Runnable
metadata: Optional[dict[str, Any]]
input: Type[Any]
retry_policy: Optional[RetryPolicy]
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]]
ends: Optional[Union[tuple[str, ...], dict[str, str]]] = EMPTY_SEQ
@@ -251,7 +251,7 @@ class StateGraph(Graph):
*,
metadata: Optional[dict[str, Any]] = None,
input: Optional[Type[Any]] = None,
retry: Optional[RetryPolicy] = None,
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
) -> Self:
"""Adds a new node to the state graph.
@@ -276,7 +276,7 @@ class StateGraph(Graph):
*,
metadata: Optional[dict[str, Any]] = None,
input: Optional[Type[Any]] = None,
retry: Optional[RetryPolicy] = None,
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
) -> Self:
"""Adds a new node to the state graph.
@@ -300,7 +300,7 @@ class StateGraph(Graph):
*,
metadata: Optional[dict[str, Any]] = None,
input: Optional[Type[Any]] = None,
retry: Optional[RetryPolicy] = None,
retry: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
) -> Self:
"""Adds a new node to the state graph.
@@ -312,7 +312,8 @@ class StateGraph(Graph):
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)
retry (Optional[RetryPolicy]): The policy for retrying the node. (default: None)
retry (Optional[Union[RetryPolicy, Sequence[RetryPolicy]]]): The policy for retrying the node. (default: None)
If a sequence is provided, the first matching policy will be applied.
destinations (Optional[Union[dict[str, str], tuple[str, ...]]]): Destinations that indicate where a node can route to.
This is useful for edgeless graphs with nodes that return `Command` objects.
If a dict is provided, the keys will be used as the target node names and the values will be used as the labels for the edges.
+7 -4
View File
@@ -498,8 +498,8 @@ class Pregel(PregelProtocol):
store: Optional[BaseStore] = None
"""Memory store to use for SharedValues. Defaults to None."""
retry_policy: Optional[RetryPolicy] = None
"""Retry policy to use when running tasks. Set to None to disable."""
retry_policy: Optional[Sequence[RetryPolicy]] = None
"""Retry policies to use when running tasks. Set to None to disable."""
config_type: Optional[Type[Any]] = None
@@ -528,7 +528,7 @@ class Pregel(PregelProtocol):
debug: Optional[bool] = None,
checkpointer: Optional[BaseCheckpointSaver] = None,
store: Optional[BaseStore] = None,
retry_policy: Optional[RetryPolicy] = None,
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
config_type: Optional[Type[Any]] = None,
input_model: Optional[Type[BaseModel]] = None,
config: Optional[RunnableConfig] = None,
@@ -548,7 +548,10 @@ class Pregel(PregelProtocol):
self.debug = debug if debug is not None else get_debug()
self.checkpointer = checkpointer
self.store = store
self.retry_policy = retry_policy
if isinstance(retry_policy, RetryPolicy):
self.retry_policy: Sequence[RetryPolicy] = (retry_policy,)
else:
self.retry_policy = retry_policy
self.config_type = config_type
self.input_model = input_model
self.config = config
+2 -2
View File
@@ -115,7 +115,7 @@ class Call:
func: Callable
input: Any
retry: Optional[RetryPolicy]
retry: Optional[Sequence[RetryPolicy]]
callbacks: Callbacks
def __init__(
@@ -123,7 +123,7 @@ class Call:
func: Callable,
input: Any,
*,
retry: Optional[RetryPolicy],
retry: Optional[Sequence[RetryPolicy]],
callbacks: Callbacks,
) -> None:
self.func = func
+2 -2
View File
@@ -5,7 +5,7 @@ import functools
import inspect
import sys
import types
from typing import Any, Callable, Generator, Generic, Optional, TypeVar, cast
from typing import Any, Callable, Generator, Generic, Optional, Sequence, TypeVar, cast
from langchain_core.runnables import Runnable
from typing_extensions import ParamSpec
@@ -224,7 +224,7 @@ class SyncAsyncFuture(Generic[T], concurrent.futures.Future[T]):
def call(
func: Callable[P, T],
*args: Any,
retry: Optional[RetryPolicy] = None,
retry: Optional[Sequence[RetryPolicy]] = None,
**kwargs: Any,
) -> SyncAsyncFuture[T]:
config = get_config()
+7 -4
View File
@@ -144,8 +144,8 @@ class PregelNode(Runnable):
"""The main logic of the node. This will be invoked with the input from
`channels`."""
retry_policy: Optional[RetryPolicy]
"""The retry policy to use when invoking the node."""
retry_policy: Optional[Sequence[RetryPolicy]]
"""The retry policies to use when invoking the node."""
tags: Optional[Sequence[str]]
"""Tags to attach to the node for tracing."""
@@ -166,7 +166,7 @@ class PregelNode(Runnable):
tags: Optional[list[str]] = None,
metadata: Optional[Mapping[str, Any]] = None,
bound: Optional[Runnable[Any, Any]] = None,
retry_policy: Optional[RetryPolicy] = None,
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
subgraphs: Optional[Sequence[PregelProtocol]] = None,
) -> None:
self.channels = channels
@@ -174,7 +174,10 @@ class PregelNode(Runnable):
self.mapper = mapper
self.writers = writers or []
self.bound = bound if bound is not None else DEFAULT_BOUND
self.retry_policy = retry_policy
if isinstance(retry_policy, RetryPolicy):
self.retry_policy: Sequence[RetryPolicy] = (retry_policy,)
else:
self.retry_policy = retry_policy
self.tags = tags
self.metadata = metadata
if subgraphs is not None:
+66 -50
View File
@@ -22,12 +22,11 @@ SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
def run_with_retry(
task: PregelExecutableTask,
retry_policy: Optional[RetryPolicy],
retry_policy: Optional[Sequence[RetryPolicy]],
configurable: Optional[dict[str, Any]] = None,
) -> None:
"""Run a task with retries."""
retry_policy = task.retry_policy or retry_policy
interval = retry_policy.initial_interval if retry_policy else 0
attempts = 0
config = task.config
if configurable is not None:
@@ -63,38 +62,39 @@ def run_with_retry(
exc.add_note(f"During task with name '{task.name}' and id '{task.id}'")
if retry_policy is None:
raise
# Check which retry policy applies to this exception
matching_policy = None
for policy in retry_policy:
if _should_retry_on(policy, exc):
matching_policy = policy
break
if not matching_policy:
raise
# increment attempts
attempts += 1
# check if we should retry
if isinstance(retry_policy.retry_on, Sequence):
if not isinstance(exc, tuple(retry_policy.retry_on)):
raise
elif isinstance(retry_policy.retry_on, type) and issubclass(
retry_policy.retry_on, Exception
):
if not isinstance(exc, retry_policy.retry_on):
raise
elif callable(retry_policy.retry_on):
if not retry_policy.retry_on(exc): # type: ignore[call-arg]
raise
else:
raise TypeError(
"retry_on must be an Exception class, a list or tuple of Exception classes, or a callable"
)
# check if we should give up
if attempts >= retry_policy.max_attempts:
if attempts >= matching_policy.max_attempts:
raise
# sleep before retrying
interval = matching_policy.initial_interval
# Apply backoff factor based on attempt count
interval = min(
retry_policy.max_interval,
interval * retry_policy.backoff_factor,
matching_policy.max_interval,
interval * (matching_policy.backoff_factor ** (attempts - 1)),
)
time.sleep(
interval + random.uniform(0, 1) if retry_policy.jitter else interval
# Apply jitter if configured
sleep_time = (
interval + random.uniform(0, 1) if matching_policy.jitter else interval
)
time.sleep(sleep_time)
# log the retry
logger.info(
f"Retrying task {task.name} after {interval:.2f} seconds (attempt {attempts}) after {exc.__class__.__name__} {exc}",
f"Retrying task {task.name} after {sleep_time:.2f} seconds (attempt {attempts}) after {exc.__class__.__name__} {exc}",
exc_info=exc,
)
# signal subgraphs to resume (if available)
@@ -103,13 +103,12 @@ def run_with_retry(
async def arun_with_retry(
task: PregelExecutableTask,
retry_policy: Optional[RetryPolicy],
retry_policies: Optional[Sequence[RetryPolicy]],
stream: bool = False,
configurable: Optional[dict[str, Any]] = None,
) -> None:
"""Run a task asynchronously with retries."""
retry_policy = task.retry_policy or retry_policy
interval = retry_policy.initial_interval if retry_policy else 0
retry_policies = task.retry_policy or retry_policies
attempts = 0
config = task.config
if configurable is not None:
@@ -149,41 +148,58 @@ async def arun_with_retry(
except Exception as exc:
if SUPPORTS_EXC_NOTES:
exc.add_note(f"During task with name '{task.name}' and id '{task.id}'")
if retry_policy is None:
if retry_policies is None:
raise
# Check which retry policy applies to this exception
matching_policy = None
for policy in retry_policies:
if _should_retry_on(policy, exc):
matching_policy = policy
break
if not matching_policy:
raise
# increment attempts
attempts += 1
# check if we should retry
if isinstance(retry_policy.retry_on, Sequence):
if not isinstance(exc, tuple(retry_policy.retry_on)):
raise
elif isinstance(retry_policy.retry_on, type) and issubclass(
retry_policy.retry_on, Exception
):
if not isinstance(exc, retry_policy.retry_on):
raise
elif callable(retry_policy.retry_on):
if not retry_policy.retry_on(exc): # type: ignore[call-arg]
raise
else:
raise TypeError(
"retry_on must be an Exception class, a list or tuple of Exception classes, or a callable"
)
# check if we should give up
if attempts >= retry_policy.max_attempts:
if attempts >= matching_policy.max_attempts:
raise
# sleep before retrying
interval = matching_policy.initial_interval
# Apply backoff factor based on attempt count
interval = min(
retry_policy.max_interval,
interval * retry_policy.backoff_factor,
matching_policy.max_interval,
interval * (matching_policy.backoff_factor ** (attempts - 1)),
)
await asyncio.sleep(
interval + random.uniform(0, 1) if retry_policy.jitter else interval
# Apply jitter if configured
sleep_time = (
interval + random.uniform(0, 1) if matching_policy.jitter else interval
)
await asyncio.sleep(sleep_time)
# log the retry
logger.info(
f"Retrying task {task.name} after {interval:.2f} seconds (attempt {attempts}) after {exc.__class__.__name__} {exc}",
f"Retrying task {task.name} after {sleep_time:.2f} seconds (attempt {attempts}) after {exc.__class__.__name__} {exc}",
exc_info=exc,
)
# signal subgraphs to resume (if available)
config = patch_configurable(config, {CONFIG_KEY_RESUMING: True})
def _should_retry_on(retry_policy: RetryPolicy, exc: Exception) -> bool:
"""Check if the given exception should be retried based on the retry policy."""
if isinstance(retry_policy.retry_on, Sequence):
return isinstance(exc, tuple(retry_policy.retry_on))
elif isinstance(retry_policy.retry_on, type) and issubclass(
retry_policy.retry_on, Exception
):
return isinstance(exc, retry_policy.retry_on)
elif callable(retry_policy.retry_on):
return retry_policy.retry_on(exc) # type: ignore[call-arg]
else:
raise TypeError(
"retry_on must be an Exception class, a list or tuple of Exception classes, or a callable"
)
+4 -4
View File
@@ -140,7 +140,7 @@ class PregelRunner:
*,
reraise: bool = True,
timeout: Optional[float] = None,
retry_policy: Optional[RetryPolicy] = None,
retry_policy: Optional[Sequence[RetryPolicy]] = None,
get_waiter: Optional[Callable[[], concurrent.futures.Future[None]]] = None,
) -> Iterator[None]:
tasks = tuple(tasks)
@@ -269,7 +269,7 @@ class PregelRunner:
*,
reraise: bool = True,
timeout: Optional[float] = None,
retry_policy: Optional[RetryPolicy] = None,
retry_policy: Optional[Sequence[RetryPolicy]] = None,
get_waiter: Optional[Callable[[], asyncio.Future[None]]] = None,
) -> AsyncIterator[None]:
loop = asyncio.get_event_loop()
@@ -519,7 +519,7 @@ def _call(
func: Callable[[Any], Union[Awaitable[Any], Any]],
input: Any,
*,
retry: Optional[RetryPolicy] = None,
retry: Optional[Sequence[RetryPolicy]] = None,
callbacks: Callbacks = None,
futures: weakref.ref[FuturesDict],
schedule_task: weakref.ref[
@@ -600,7 +600,7 @@ def _acall(
func: Callable[[Any], Union[Awaitable[Any], Any]],
input: Any,
*,
retry: Optional[RetryPolicy] = None,
retry: Optional[Sequence[RetryPolicy]] = None,
callbacks: Callbacks = None,
# injected dependencies
futures: weakref.ref[FuturesDict],
+5 -5
View File
@@ -75,6 +75,10 @@ def default_retry_on(exc: Exception) -> bool:
if isinstance(exc, ConnectionError):
return True
if isinstance(exc, httpx.HTTPStatusError):
return 500 <= exc.response.status_code < 600
if isinstance(exc, requests.HTTPError):
return 500 <= exc.response.status_code < 600 if exc.response else True
if isinstance(
exc,
(
@@ -93,10 +97,6 @@ def default_retry_on(exc: Exception) -> bool:
),
):
return False
if isinstance(exc, httpx.HTTPStatusError):
return 500 <= exc.response.status_code < 600
if isinstance(exc, requests.HTTPError):
return 500 <= exc.response.status_code < 600 if exc.response else True
return True
@@ -172,7 +172,7 @@ class PregelExecutableTask:
writes: deque[tuple[str, Any]]
config: RunnableConfig
triggers: Sequence[str]
retry_policy: Optional[RetryPolicy]
retry_policy: Optional[Sequence[RetryPolicy]]
cache_policy: Optional[CachePolicy]
id: str
path: tuple[Union[str, int, tuple], ...]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.3.27"
version = "0.3.28"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
+342
View File
@@ -0,0 +1,342 @@
from unittest.mock import Mock, patch
import pytest
from typing_extensions import TypedDict
from langgraph.graph import START, StateGraph
from langgraph.pregel.retry import _should_retry_on
from langgraph.types import RetryPolicy
def test_should_retry_on_single_exception():
"""Test retry with a single exception type."""
policy = RetryPolicy(retry_on=ValueError)
# Should retry on ValueError
assert _should_retry_on(policy, ValueError("test error")) is True
# Should not retry on other exceptions
assert _should_retry_on(policy, TypeError("test error")) is False
assert _should_retry_on(policy, Exception("test error")) is False
def test_should_retry_on_sequence_of_exceptions():
"""Test retry with a sequence of exception types."""
policy = RetryPolicy(retry_on=(ValueError, KeyError))
# Should retry on listed exceptions
assert _should_retry_on(policy, ValueError("test error")) is True
assert _should_retry_on(policy, KeyError("test error")) is True
# Should not retry on other exceptions
assert _should_retry_on(policy, TypeError("test error")) is False
assert _should_retry_on(policy, Exception("test error")) is False
def test_should_retry_on_subclass_of_exception():
"""Test retry on subclass of specified exception."""
class CustomError(ValueError):
pass
policy = RetryPolicy(retry_on=ValueError)
# Should retry on subclass of specified exception
assert _should_retry_on(policy, CustomError("test error")) is True
def test_should_retry_on_callable():
"""Test retry with a callable predicate."""
# Only retry on ValueError with message containing 'retry'
def should_retry(exc: Exception) -> bool:
return isinstance(exc, ValueError) and "retry" in str(exc)
policy = RetryPolicy(retry_on=should_retry)
# Should retry when predicate returns True
assert _should_retry_on(policy, ValueError("please retry this")) is True
# Should not retry when predicate returns False
assert _should_retry_on(policy, ValueError("other error")) is False
assert _should_retry_on(policy, TypeError("please retry this")) is False
def test_should_retry_on_invalid_type():
"""Test retry with an invalid retry_on type."""
policy = RetryPolicy(retry_on=123) # type: ignore
with pytest.raises(TypeError, match="retry_on must be an Exception class"):
_should_retry_on(policy, ValueError("test error"))
def test_should_retry_on_empty_sequence():
"""Test retry with an empty sequence."""
policy = RetryPolicy(retry_on=())
# Should not retry when sequence is empty
assert _should_retry_on(policy, ValueError("test error")) is False
def test_should_retry_default_retry_on():
"""Test the default retry_on function."""
import httpx
import requests
# Create a RetryPolicy with default_retry_on
policy = RetryPolicy()
# Should retry on ConnectionError
assert _should_retry_on(policy, ConnectionError("connection refused")) is True
# Should not retry on common programming errors
assert _should_retry_on(policy, ValueError("invalid value")) is False
assert _should_retry_on(policy, TypeError("invalid type")) is False
assert _should_retry_on(policy, ArithmeticError("division by zero")) is False
assert _should_retry_on(policy, ImportError("module not found")) is False
assert _should_retry_on(policy, LookupError("key not found")) is False
assert _should_retry_on(policy, NameError("name not defined")) is False
assert _should_retry_on(policy, SyntaxError("invalid syntax")) is False
assert _should_retry_on(policy, RuntimeError("runtime error")) is False
assert _should_retry_on(policy, ReferenceError("weak reference")) is False
assert _should_retry_on(policy, StopIteration()) is False
assert _should_retry_on(policy, StopAsyncIteration()) is False
assert _should_retry_on(policy, OSError("file not found")) is False
# Should retry on httpx.HTTPStatusError with 5xx status code
response_5xx = Mock()
response_5xx.status_code = 503
http_error_5xx = httpx.HTTPStatusError(
"server error", request=Mock(), response=response_5xx
)
assert _should_retry_on(policy, http_error_5xx) is True
# Should not retry on httpx.HTTPStatusError with 4xx status code
response_4xx = Mock()
response_4xx.status_code = 404
http_error_4xx = httpx.HTTPStatusError(
"not found", request=Mock(), response=response_4xx
)
assert _should_retry_on(policy, http_error_4xx) is False
# Should retry on requests.HTTPError with 5xx status code
response_req_5xx = Mock()
response_req_5xx.status_code = 502
req_error_5xx = requests.HTTPError("bad gateway")
req_error_5xx.response = response_req_5xx
assert _should_retry_on(policy, req_error_5xx) is True
# Should not retry on requests.HTTPError with 4xx status code
response_req_4xx = Mock()
response_req_4xx.status_code = 400
req_error_4xx = requests.HTTPError("bad request")
req_error_4xx.response = response_req_4xx
assert _should_retry_on(policy, req_error_4xx) is False
# Should retry on requests.HTTPError with no response
req_error_no_resp = requests.HTTPError("connection error")
req_error_no_resp.response = None
assert _should_retry_on(policy, req_error_no_resp) is True
# Should retry on other exceptions by default
class CustomException(Exception):
pass
assert _should_retry_on(policy, CustomException("custom error")) is True
def test_graph_with_single_retry_policy():
"""Test a simple graph with a single RetryPolicy for a node."""
class State(TypedDict):
foo: str
attempt_count = 0
def failing_node(state: State):
nonlocal attempt_count
attempt_count += 1
if attempt_count < 3: # Fail the first two attempts
raise ValueError("Intentional failure")
return {"foo": "success"}
def other_node(state: State):
return {"foo": "other_node"}
# Create a retry policy with specific parameters
retry_policy = RetryPolicy(
max_attempts=3,
initial_interval=0.01, # Short interval for tests
backoff_factor=2.0,
jitter=False, # Disable jitter for predictable timing
retry_on=ValueError,
)
# Create and compile the graph
graph = (
StateGraph(State)
.add_node("failing_node", failing_node, retry=retry_policy)
.add_node("other_node", other_node)
.add_edge(START, "failing_node")
.add_edge("failing_node", "other_node")
.compile()
)
with patch("time.sleep") as mock_sleep:
result = graph.invoke({"foo": ""})
# Verify retry behavior
assert attempt_count == 3 # The node should have been tried 3 times
assert result["foo"] == "other_node" # Final result should be from other_node
# Verify the sleep intervals
call_args_list = [args[0][0] for args in mock_sleep.call_args_list]
assert call_args_list == [0.01, 0.02]
def test_graph_with_jitter_retry_policy():
"""Test a graph with a RetryPolicy that uses jitter."""
class State(TypedDict):
foo: str
attempt_count = 0
def failing_node(state):
nonlocal attempt_count
attempt_count += 1
if attempt_count < 2: # Fail the first attempt
raise ValueError("Intentional failure")
return {"foo": "success"}
# Create a retry policy with jitter enabled
retry_policy = RetryPolicy(
max_attempts=3,
initial_interval=0.01,
jitter=True, # Enable jitter for randomized backoff
retry_on=ValueError,
)
# Create and compile the graph
graph = (
StateGraph(State)
.add_node("failing_node", failing_node, retry=retry_policy)
.add_edge(START, "failing_node")
.compile()
)
# Test graph execution with mocked random and sleep
with patch("random.uniform", return_value=0.05) as mock_random, patch(
"time.sleep"
) as mock_sleep:
result = graph.invoke({"foo": ""})
# Verify retry behavior
assert attempt_count == 2 # The node should have been tried twice
assert result["foo"] == "success"
# Verify jitter was applied
mock_random.assert_called_with(0, 1) # Jitter should use random.uniform(0, 1)
mock_sleep.assert_called_with(0.01 + 0.05) # Sleep should include jitter
def test_graph_with_multiple_retry_policies():
"""Test a graph with multiple retry policies for a node."""
class State(TypedDict):
foo: str
error_type: str
attempt_counts = {"value_error": 0, "key_error": 0}
def failing_node(state):
error_type = state["error_type"]
if error_type == "value_error":
attempt_counts["value_error"] += 1
if attempt_counts["value_error"] < 2:
raise ValueError("Value error")
elif error_type == "key_error":
attempt_counts["key_error"] += 1
if attempt_counts["key_error"] < 3:
raise KeyError("Key error")
return {"foo": f"recovered_from_{error_type}"}
# Create multiple retry policies
value_error_policy = RetryPolicy(
max_attempts=2,
initial_interval=0.01,
jitter=False,
retry_on=ValueError,
)
key_error_policy = RetryPolicy(
max_attempts=3,
initial_interval=0.02,
jitter=False,
retry_on=KeyError,
)
# Create and compile the graph with a list of retry policies
graph = (
StateGraph(State)
.add_node(
"failing_node",
failing_node,
retry=(value_error_policy, key_error_policy),
)
.add_edge(START, "failing_node")
.compile()
)
# Test ValueError scenario
with patch("time.sleep"):
result_value_error = graph.invoke({"foo": "", "error_type": "value_error"})
assert attempt_counts["value_error"] == 2
assert result_value_error["foo"] == "recovered_from_value_error"
# Reset attempt counts
attempt_counts = {"value_error": 0, "key_error": 0}
# Test KeyError scenario
with patch("time.sleep"):
result_key_error = graph.invoke({"foo": "", "error_type": "key_error"})
assert attempt_counts["key_error"] == 3
assert result_key_error["foo"] == "recovered_from_key_error"
def test_graph_with_max_attempts_exceeded():
"""Test a graph where max_attempts is exceeded."""
class State(TypedDict):
foo: str
def always_failing_node(state):
raise ValueError("Always fails")
# Create a retry policy with limited attempts
retry_policy = RetryPolicy(
max_attempts=2,
initial_interval=0.01,
jitter=False,
retry_on=ValueError,
)
# Create and compile the graph
graph = (
StateGraph(State)
.add_node("always_failing", always_failing_node, retry=retry_policy)
.add_edge(START, "always_failing")
.compile()
)
# Test graph execution
with patch("time.sleep") as mock_sleep, pytest.raises(
ValueError, match="Always fails"
):
graph.invoke({"foo": ""})
mock_sleep.assert_called_with(0.01)
+1 -1
View File
@@ -11,7 +11,7 @@
"format": "prettier --write src",
"lint": "prettier --check src && tsc --noEmit",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts",
"typedoc": "typedoc && typedoc src/react/index.ts --out docs/react --options typedoc.react.json"
"typedoc": "typedoc && typedoc src/react/index.ts --out docs/react --options typedoc.react.json && typedoc src/auth/index.ts --out docs/auth --options typedoc.auth.json"
},
"main": "index.js",
"license": "MIT",
+4
View File
@@ -13,6 +13,10 @@ export class Auth<
TAuthReturn extends BaseAuthReturn = BaseAuthReturn,
TUser extends BaseUser = ToUserLike<TAuthReturn>,
> {
/**
* @internal
* @ignore
*/
"~handlerCache": {
authenticate?: AuthenticateCallback<BaseAuthReturn>;
callbacks?: Record<string, AnyCallback>;
+63
View File
@@ -11,6 +11,9 @@ interface AssistantConfig {
}>;
}
/**
* @inline
*/
interface AssistantCreate {
assistant_id?: Maybe<string>;
metadata?: Maybe<Record<string, unknown>>;
@@ -20,11 +23,17 @@ interface AssistantCreate {
graph_id: string;
}
/**
* @inline
*/
interface AssistantRead {
assistant_id: string;
metadata?: Maybe<Record<string, unknown>>;
}
/**
* @inline
*/
interface AssistantUpdate {
assistant_id: string;
metadata?: Maybe<Record<string, unknown>>;
@@ -34,10 +43,16 @@ interface AssistantUpdate {
version?: Maybe<number>;
}
/**
* @inline
*/
interface AssistantDelete {
assistant_id: string;
}
/**
* @inline
*/
interface AssistantSearch {
graph_id?: Maybe<string>;
metadata?: Maybe<Record<string, unknown>>;
@@ -45,27 +60,42 @@ interface AssistantSearch {
offset?: Maybe<number>;
}
/**
* @inline
*/
interface ThreadCreate {
thread_id?: Maybe<string>;
metadata?: Maybe<Record<string, unknown>>;
if_exists?: Maybe<"raise" | "do_nothing">;
}
/**
* @inline
*/
interface ThreadRead {
thread_id?: Maybe<string>;
}
/**
* @inline
*/
interface ThreadUpdate {
thread_id?: Maybe<string>;
metadata?: Maybe<Record<string, unknown>>;
action?: Maybe<"interrupt" | "rollback">;
}
/**
* @inline
*/
interface ThreadDelete {
thread_id?: Maybe<string>;
run_id?: Maybe<string>;
}
/**
* @inline
*/
interface ThreadSearch {
thread_id?: Maybe<string>;
status?: Maybe<"idle" | "busy" | "interrupted" | "error" | (string & {})>;
@@ -75,6 +105,9 @@ interface ThreadSearch {
offset?: Maybe<number>;
}
/**
* @inline
*/
interface CronCreate {
payload?: Maybe<Record<string, unknown>>;
schedule: string;
@@ -84,20 +117,32 @@ interface CronCreate {
end_time?: Maybe<string>;
}
/**
* @inline
*/
interface CronRead {
cron_id: string;
}
/**
* @inline
*/
interface CronUpdate {
cron_id: string;
payload?: Maybe<Record<string, unknown>>;
schedule?: Maybe<string>;
}
/**
* @inline
*/
interface CronDelete {
cron_id: string;
}
/**
* @inline
*/
interface CronSearch {
assistant_id?: Maybe<string>;
thread_id?: Maybe<string>;
@@ -105,17 +150,26 @@ interface CronSearch {
offset?: Maybe<number>;
}
/**
* @inline
*/
interface StorePut {
namespace: string[];
key: string;
value: Record<string, unknown>;
}
/**
* @inline
*/
interface StoreGet {
namespace: Maybe<string[]>;
key: string;
}
/**
* @inline
*/
interface StoreSearch {
namespace?: Maybe<string[]>;
filter?: Maybe<Record<string, unknown>>;
@@ -124,6 +178,9 @@ interface StoreSearch {
query?: Maybe<string>;
}
/**
* @inline
*/
interface StoreListNamespaces {
namespace?: Maybe<string[]>;
suffix?: Maybe<string[]>;
@@ -132,11 +189,17 @@ interface StoreListNamespaces {
offset?: Maybe<number>;
}
/**
* @inline
*/
interface StoreDelete {
namespace?: Maybe<string[]>;
key: string;
}
/**
* @inline
*/
interface RunsCreate {
thread_id?: Maybe<string>;
assistant_id: string;
+5
View File
@@ -0,0 +1,5 @@
{
"pageTitleTemplates": {
"index": "{projectName}/auth"
}
}