mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-28 10:49:56 +02:00
done
This commit is contained in:
@@ -27,6 +27,7 @@ from langgraph_cli.api_version import resolve_langgraph_api_version
|
||||
from langgraph_cli.config import Config
|
||||
from langgraph_cli.constants import DEFAULT_CONFIG, DEFAULT_PORT
|
||||
from langgraph_cli.docker import DockerCapabilities
|
||||
from langgraph_cli.engine_runtime_mode import resolve_engine_runtime_mode
|
||||
from langgraph_cli.exec import Runner, subp_exec
|
||||
from langgraph_cli.host_backend import HostBackendClient, HostBackendError
|
||||
from langgraph_cli.progress import Progress
|
||||
@@ -292,8 +293,8 @@ OPT_API_VERSION = click.option(
|
||||
OPT_ENGINE_RUNTIME_MODE = click.option(
|
||||
"--engine-runtime-mode",
|
||||
type=click.Choice(["combined_queue_worker", "distributed"]),
|
||||
default="combined_queue_worker",
|
||||
help="Runtime mode. 'distributed' uses separate executor and orchestrator containers.",
|
||||
default=None,
|
||||
help="Runtime mode. 'distributed' uses separate executor and orchestrator containers. Defaults to distributed.",
|
||||
)
|
||||
|
||||
|
||||
@@ -349,11 +350,14 @@ def up(
|
||||
debugger_base_url: str | None,
|
||||
postgres_uri: str | None,
|
||||
api_version: str | None,
|
||||
engine_runtime_mode: str,
|
||||
engine_runtime_mode: str | None,
|
||||
image: str | None,
|
||||
base_image: str | None,
|
||||
):
|
||||
api_version = resolve_langgraph_api_version(config, api_version)
|
||||
engine_runtime_mode = resolve_engine_runtime_mode(
|
||||
config, api_version, engine_runtime_mode
|
||||
)
|
||||
click.secho("Starting LangGraph API server...", fg="green")
|
||||
click.secho(
|
||||
"""For local dev, requires env var LANGSMITH_API_KEY with access to LangSmith Deployment.
|
||||
@@ -553,13 +557,16 @@ def build(
|
||||
docker_build_args: Sequence[str],
|
||||
base_image: str | None,
|
||||
api_version: str | None,
|
||||
engine_runtime_mode: str,
|
||||
engine_runtime_mode: str | None,
|
||||
pull: bool,
|
||||
tag: str,
|
||||
install_command: str | None,
|
||||
build_command: str | None,
|
||||
):
|
||||
api_version = resolve_langgraph_api_version(config, api_version)
|
||||
engine_runtime_mode = resolve_engine_runtime_mode(
|
||||
config, api_version, engine_runtime_mode
|
||||
)
|
||||
if install_command and langgraph_cli.config.has_disallowed_build_command_content(
|
||||
install_command
|
||||
):
|
||||
@@ -692,7 +699,8 @@ def deploy(
|
||||
no_wait: bool,
|
||||
docker_build_args: Sequence[str],
|
||||
):
|
||||
deploy_api_version = resolve_langgraph_api_version(config, api_version)
|
||||
api_version = resolve_langgraph_api_version(config, api_version)
|
||||
engine_runtime_mode = resolve_engine_runtime_mode(config, api_version, None)
|
||||
click.secho(
|
||||
"Note: 'langgraph deploy' is in beta. Expect frequent updates and improvements.",
|
||||
fg="yellow",
|
||||
@@ -720,12 +728,6 @@ def deploy(
|
||||
|
||||
secrets = _secrets_from_env(env_vars)
|
||||
|
||||
# Determine language and runtime mode from config for host backend
|
||||
is_python = bool(config_json.get("python_version")) or not config_json.get(
|
||||
"node_version"
|
||||
)
|
||||
deploy_engine_runtime_mode = "distributed" if is_python else "combined_queue_server"
|
||||
|
||||
# Use buildx to cross-compile for amd64 when running on a non-x86_64 host
|
||||
# (e.g. Apple Silicon). On amd64 hosts, plain docker build is sufficient.
|
||||
needs_buildx = platform.machine() != "x86_64"
|
||||
@@ -875,10 +877,10 @@ def deploy(
|
||||
"source_config": {"deployment_type": deployment_type},
|
||||
"source_revision_config": {},
|
||||
"secrets": secrets,
|
||||
"engine_runtime_mode": deploy_engine_runtime_mode,
|
||||
"engine_runtime_mode": engine_runtime_mode,
|
||||
}
|
||||
if deploy_api_version:
|
||||
payload["deployed_api_version"] = deploy_api_version
|
||||
if api_version:
|
||||
payload["deployed_api_version"] = api_version
|
||||
created = client.create_deployment(payload)
|
||||
created_id = created.get("id") if isinstance(created, dict) else None
|
||||
if not isinstance(created_id, str) or not created_id:
|
||||
@@ -991,8 +993,8 @@ def deploy(
|
||||
deployment_id,
|
||||
remote_image,
|
||||
secrets=secrets,
|
||||
engine_runtime_mode=deploy_engine_runtime_mode,
|
||||
deployed_api_version=deploy_api_version,
|
||||
engine_runtime_mode=engine_runtime_mode,
|
||||
deployed_api_version=api_version,
|
||||
)
|
||||
tenant_id = updated.get("tenant_id") if isinstance(updated, dict) else None
|
||||
if tenant_id:
|
||||
@@ -1181,9 +1183,12 @@ def dockerfile(
|
||||
add_docker_compose: bool,
|
||||
base_image: str | None = None,
|
||||
api_version: str | None = None,
|
||||
engine_runtime_mode: str = "combined_queue_worker",
|
||||
engine_runtime_mode: str | None = None,
|
||||
) -> None:
|
||||
api_version = resolve_langgraph_api_version(config, api_version)
|
||||
engine_runtime_mode = resolve_engine_runtime_mode(
|
||||
config, api_version, engine_runtime_mode
|
||||
)
|
||||
save_path = pathlib.Path(save_path).absolute()
|
||||
secho(f"🔍 Validating configuration at path: {config}", fg="yellow")
|
||||
config_json = langgraph_cli.config.validate_config_file(config)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Resolve the LangGraph engine runtime mode."""
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
import click
|
||||
|
||||
_DISTRIBUTED_MIN_VERSION = (0, 7, 68)
|
||||
|
||||
|
||||
def resolve_engine_runtime_mode(
|
||||
config_path: pathlib.Path,
|
||||
api_version: str,
|
||||
engine_runtime_mode_cli_param: str | None,
|
||||
) -> str:
|
||||
"""Resolve the engine runtime mode.
|
||||
|
||||
*api_version* must already be resolved to a patch-level semver string.
|
||||
|
||||
Returns ``"distributed"`` or ``"combined_queue_worker"``.
|
||||
|
||||
Raises `click.ClickException` when distributed mode is requested but
|
||||
not supported (JavaScript project or api_version <= 0.7.67).
|
||||
"""
|
||||
requires_combined = _requires_combined(config_path, api_version)
|
||||
|
||||
if engine_runtime_mode_cli_param == "distributed":
|
||||
if requires_combined:
|
||||
reasons = _constraint_reasons(config_path, api_version)
|
||||
raise click.ClickException(
|
||||
f"Distributed runtime is not supported for {' and '.join(reasons)}."
|
||||
)
|
||||
return "distributed"
|
||||
|
||||
if engine_runtime_mode_cli_param == "combined_queue_worker":
|
||||
return "combined_queue_worker"
|
||||
|
||||
# No explicit choice → default to distributed
|
||||
return "distributed"
|
||||
|
||||
|
||||
def _requires_combined(config_path: pathlib.Path, api_version: str) -> bool:
|
||||
return _is_javascript_project(config_path) or _version_too_old(api_version)
|
||||
|
||||
|
||||
def _version_too_old(api_version: str) -> bool:
|
||||
try:
|
||||
parts = tuple(int(x) for x in api_version.split("."))
|
||||
except (ValueError, AttributeError):
|
||||
return True
|
||||
return parts < _DISTRIBUTED_MIN_VERSION
|
||||
|
||||
|
||||
def _is_javascript_project(config_path: pathlib.Path) -> bool:
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
cfg = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False
|
||||
return bool(cfg.get("node_version")) and not cfg.get("python_version")
|
||||
|
||||
|
||||
def _constraint_reasons(config_path: pathlib.Path, api_version: str) -> list[str]:
|
||||
reasons: list[str] = []
|
||||
if _is_javascript_project(config_path):
|
||||
reasons.append("JavaScript projects")
|
||||
if _version_too_old(api_version):
|
||||
reasons.append(f"API version {api_version} (<= 0.7.67)")
|
||||
return reasons
|
||||
@@ -568,6 +568,8 @@ def test_build_generate_proper_build_context():
|
||||
"test-image",
|
||||
"--config",
|
||||
str(temp_dir / "config.json"),
|
||||
"--engine-runtime-mode",
|
||||
"combined_queue_worker",
|
||||
],
|
||||
catch_exceptions=True,
|
||||
)
|
||||
@@ -603,6 +605,8 @@ def test_dockerfile_command_with_api_version() -> None:
|
||||
str(temp_dir / "config.json"),
|
||||
"--api-version",
|
||||
"0.2.74",
|
||||
"--engine-runtime-mode",
|
||||
"combined_queue_worker",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -719,6 +723,8 @@ def test_build_command_with_api_version() -> None:
|
||||
str(temp_dir / "config.json"),
|
||||
"--api-version",
|
||||
"0.2.74",
|
||||
"--engine-runtime-mode",
|
||||
"combined_queue_worker",
|
||||
"--no-pull", # Avoid pulling non-existent images
|
||||
],
|
||||
catch_exceptions=True,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
import click
|
||||
import pytest
|
||||
|
||||
from langgraph_cli.engine_runtime_mode import resolve_engine_runtime_mode
|
||||
|
||||
|
||||
def _write_config(
|
||||
tmp_path: pathlib.Path,
|
||||
*,
|
||||
python_version: str | None = "3.11",
|
||||
node_version: str | None = None,
|
||||
) -> pathlib.Path:
|
||||
cfg: dict = {"dependencies": ["."], "graphs": {"agent": "agent.py:graph"}}
|
||||
if python_version is not None:
|
||||
cfg["python_version"] = python_version
|
||||
if node_version is not None:
|
||||
cfg["node_version"] = node_version
|
||||
path = tmp_path / "langgraph.json"
|
||||
path.write_text(json.dumps(cfg))
|
||||
return path
|
||||
|
||||
|
||||
class TestResolveEngineRuntimeMode:
|
||||
# -- cli_param == "distributed" -------------------------------------------
|
||||
|
||||
def test_distributed_explicit_new_version(self, tmp_path: pathlib.Path) -> None:
|
||||
path = _write_config(tmp_path)
|
||||
assert (
|
||||
resolve_engine_runtime_mode(path, "0.7.68", "distributed") == "distributed"
|
||||
)
|
||||
|
||||
def test_distributed_explicit_old_version_raises(
|
||||
self, tmp_path: pathlib.Path
|
||||
) -> None:
|
||||
path = _write_config(tmp_path)
|
||||
with pytest.raises(click.ClickException, match="0.7.67"):
|
||||
resolve_engine_runtime_mode(path, "0.7.67", "distributed")
|
||||
|
||||
def test_distributed_explicit_js_raises(self, tmp_path: pathlib.Path) -> None:
|
||||
path = _write_config(tmp_path, python_version=None, node_version="20")
|
||||
with pytest.raises(click.ClickException, match="JavaScript"):
|
||||
resolve_engine_runtime_mode(path, "0.8.0", "distributed")
|
||||
|
||||
def test_distributed_explicit_js_and_old_version_raises(
|
||||
self, tmp_path: pathlib.Path
|
||||
) -> None:
|
||||
path = _write_config(tmp_path, python_version=None, node_version="20")
|
||||
with pytest.raises(click.ClickException, match="JavaScript.*0.7.60"):
|
||||
resolve_engine_runtime_mode(path, "0.7.60", "distributed")
|
||||
|
||||
# -- cli_param == "combined_queue_worker" ----------------------------------
|
||||
|
||||
def test_combined_explicit(self, tmp_path: pathlib.Path) -> None:
|
||||
path = _write_config(tmp_path)
|
||||
assert (
|
||||
resolve_engine_runtime_mode(path, "0.8.0", "combined_queue_worker")
|
||||
== "combined_queue_worker"
|
||||
)
|
||||
|
||||
def test_combined_explicit_old_version(self, tmp_path: pathlib.Path) -> None:
|
||||
path = _write_config(tmp_path)
|
||||
assert (
|
||||
resolve_engine_runtime_mode(path, "0.7.67", "combined_queue_worker")
|
||||
== "combined_queue_worker"
|
||||
)
|
||||
|
||||
# -- cli_param is None (default) -------------------------------------------
|
||||
|
||||
def test_default_is_distributed(self, tmp_path: pathlib.Path) -> None:
|
||||
path = _write_config(tmp_path)
|
||||
assert resolve_engine_runtime_mode(path, "0.8.0", None) == "distributed"
|
||||
|
||||
def test_default_old_version(self, tmp_path: pathlib.Path) -> None:
|
||||
path = _write_config(tmp_path)
|
||||
assert resolve_engine_runtime_mode(path, "0.7.67", None) == "distributed"
|
||||
|
||||
def test_default_js(self, tmp_path: pathlib.Path) -> None:
|
||||
path = _write_config(tmp_path, python_version=None, node_version="20")
|
||||
assert resolve_engine_runtime_mode(path, "0.8.0", None) == "distributed"
|
||||
|
||||
# -- edge: version boundary ------------------------------------------------
|
||||
|
||||
def test_version_boundary_0_7_67_blocks_distributed(
|
||||
self, tmp_path: pathlib.Path
|
||||
) -> None:
|
||||
path = _write_config(tmp_path)
|
||||
with pytest.raises(click.ClickException):
|
||||
resolve_engine_runtime_mode(path, "0.7.67", "distributed")
|
||||
|
||||
def test_version_boundary_0_7_68_allows_distributed(
|
||||
self, tmp_path: pathlib.Path
|
||||
) -> None:
|
||||
path = _write_config(tmp_path)
|
||||
assert (
|
||||
resolve_engine_runtime_mode(path, "0.7.68", "distributed") == "distributed"
|
||||
)
|
||||
Reference in New Issue
Block a user