mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-25 02:55:07 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3de8ed373 |
@@ -15,7 +15,11 @@ from langgraph_cli.analytics import log_command
|
|||||||
from langgraph_cli.config import Config
|
from langgraph_cli.config import Config
|
||||||
from langgraph_cli.constants import DEFAULT_CONFIG, DEFAULT_PORT
|
from langgraph_cli.constants import DEFAULT_CONFIG, DEFAULT_PORT
|
||||||
from langgraph_cli.deploy import deploy
|
from langgraph_cli.deploy import deploy
|
||||||
from langgraph_cli.docker import DockerCapabilities, build_docker_image
|
from langgraph_cli.docker import (
|
||||||
|
DockerCapabilities,
|
||||||
|
build_docker_image,
|
||||||
|
get_config_to_docker_build_context,
|
||||||
|
)
|
||||||
from langgraph_cli.exec import Runner, subp_exec
|
from langgraph_cli.exec import Runner, subp_exec
|
||||||
from langgraph_cli.progress import Progress
|
from langgraph_cli.progress import Progress
|
||||||
from langgraph_cli.templates import TEMPLATE_HELP_STRING, create_new
|
from langgraph_cli.templates import TEMPLATE_HELP_STRING, create_new
|
||||||
@@ -546,6 +550,14 @@ tests
|
|||||||
)
|
)
|
||||||
@OPT_API_VERSION
|
@OPT_API_VERSION
|
||||||
@OPT_ENGINE_RUNTIME_MODE
|
@OPT_ENGINE_RUNTIME_MODE
|
||||||
|
@click.option(
|
||||||
|
"--install-command",
|
||||||
|
help="Custom install command to run from the build context root. If not provided, auto-detects based on package manager files.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--build-command",
|
||||||
|
help="Custom build command to run from the langgraph.json directory. If not provided, uses default build process.",
|
||||||
|
)
|
||||||
@log_command
|
@log_command
|
||||||
def dockerfile(
|
def dockerfile(
|
||||||
save_path: str,
|
save_path: str,
|
||||||
@@ -554,9 +566,24 @@ def dockerfile(
|
|||||||
base_image: str | None = None,
|
base_image: str | None = None,
|
||||||
api_version: str | None = None,
|
api_version: str | None = None,
|
||||||
engine_runtime_mode: str = "combined_queue_worker",
|
engine_runtime_mode: str = "combined_queue_worker",
|
||||||
|
install_command: str | None = None,
|
||||||
|
build_command: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
from click import secho
|
from click import secho
|
||||||
|
|
||||||
|
if install_command and langgraph_cli.config.has_disallowed_build_command_content(
|
||||||
|
install_command
|
||||||
|
):
|
||||||
|
raise click.UsageError(
|
||||||
|
"install_command contains disallowed characters or patterns."
|
||||||
|
)
|
||||||
|
if build_command and langgraph_cli.config.has_disallowed_build_command_content(
|
||||||
|
build_command
|
||||||
|
):
|
||||||
|
raise click.UsageError(
|
||||||
|
"build_command contains disallowed characters or patterns."
|
||||||
|
)
|
||||||
|
|
||||||
save_path = pathlib.Path(save_path).absolute()
|
save_path = pathlib.Path(save_path).absolute()
|
||||||
secho(f"🔍 Validating configuration at path: {config}", fg="yellow")
|
secho(f"🔍 Validating configuration at path: {config}", fg="yellow")
|
||||||
config_json = langgraph_cli.config.validate_config_file(config)
|
config_json = langgraph_cli.config.validate_config_file(config)
|
||||||
@@ -569,12 +596,21 @@ def dockerfile(
|
|||||||
config_json, engine_runtime_mode=engine_runtime_mode
|
config_json, engine_runtime_mode=engine_runtime_mode
|
||||||
)
|
)
|
||||||
|
|
||||||
|
build_context = get_config_to_docker_build_context(
|
||||||
|
config_json,
|
||||||
|
install_command=install_command,
|
||||||
|
build_command=build_command,
|
||||||
|
)
|
||||||
|
|
||||||
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
|
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
|
||||||
dockerfile_content, additional_contexts = langgraph_cli.config.config_to_docker(
|
dockerfile_content, additional_contexts = langgraph_cli.config.config_to_docker(
|
||||||
config_path=config,
|
config_path=config,
|
||||||
config=config_json,
|
config=config_json,
|
||||||
base_image=effective_base_image,
|
base_image=effective_base_image,
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
|
install_command=install_command,
|
||||||
|
build_command=build_command,
|
||||||
|
build_context=build_context,
|
||||||
)
|
)
|
||||||
with open(str(save_path), "w", encoding="utf-8") as f:
|
with open(str(save_path), "w", encoding="utf-8") as f:
|
||||||
f.write(dockerfile_content)
|
f.write(dockerfile_content)
|
||||||
|
|||||||
@@ -330,6 +330,20 @@ def compose(
|
|||||||
return compose_str
|
return compose_str
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_to_docker_build_context(
|
||||||
|
config_json: dict,
|
||||||
|
install_command: str | None = None,
|
||||||
|
build_command: str | None = None,
|
||||||
|
) -> str | None:
|
||||||
|
"""Return the build context passed to config_to_docker, if overridden."""
|
||||||
|
is_js_project = config_json.get("node_version") and not config_json.get(
|
||||||
|
"python_version"
|
||||||
|
)
|
||||||
|
if is_js_project and (build_command or install_command):
|
||||||
|
return str(pathlib.Path.cwd())
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def build_docker_image(
|
def build_docker_image(
|
||||||
runner,
|
runner,
|
||||||
set: Callable[[str], None],
|
set: Callable[[str], None],
|
||||||
@@ -365,16 +379,12 @@ def build_docker_image(
|
|||||||
"-t",
|
"-t",
|
||||||
tag,
|
tag,
|
||||||
]
|
]
|
||||||
# determine build context: use current directory for JS projects, config parent for Python
|
config_to_docker_build_context = get_config_to_docker_build_context(
|
||||||
is_js_project = config_json.get("node_version") and not config_json.get(
|
config_json,
|
||||||
"python_version"
|
install_command=install_command,
|
||||||
|
build_command=build_command,
|
||||||
)
|
)
|
||||||
# build/install commands only apply to JS projects for now
|
build_context = config_to_docker_build_context or str(config.parent)
|
||||||
# without install/build command, JS projects will follow the old behavior
|
|
||||||
if is_js_project and (build_command or install_command):
|
|
||||||
build_context = str(pathlib.Path.cwd())
|
|
||||||
else:
|
|
||||||
build_context = str(config.parent)
|
|
||||||
|
|
||||||
# Deep copy to avoid mutating the caller's config (config_to_docker
|
# Deep copy to avoid mutating the caller's config (config_to_docker
|
||||||
# rewrites graph paths to container-internal paths in place).
|
# rewrites graph paths to container-internal paths in place).
|
||||||
@@ -386,7 +396,7 @@ def build_docker_image(
|
|||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
install_command=install_command,
|
install_command=install_command,
|
||||||
build_command=build_command,
|
build_command=build_command,
|
||||||
build_context=build_context,
|
build_context=config_to_docker_build_context,
|
||||||
)
|
)
|
||||||
# add additional_contexts
|
# add additional_contexts
|
||||||
if additional_contexts:
|
if additional_contexts:
|
||||||
|
|||||||
@@ -1088,6 +1088,82 @@ def test_dockerfile_command_with_api_version_nodejs() -> None:
|
|||||||
assert "FROM langchain/langgraphjs-api:0.2.74-node20" in dockerfile
|
assert "FROM langchain/langgraphjs-api:0.2.74-node20" in dockerfile
|
||||||
|
|
||||||
|
|
||||||
|
def test_dockerfile_command_nodejs_monorepo_commands() -> None:
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
root = pathlib.Path.cwd()
|
||||||
|
config_dir = root / "apps" / "agent"
|
||||||
|
graph_path = config_dir / "src" / "agent.ts"
|
||||||
|
graph_path.parent.mkdir(parents=True)
|
||||||
|
graph_path.touch()
|
||||||
|
(root / "package.json").write_text(
|
||||||
|
json.dumps({"packageManager": "pnpm@10.0.0"})
|
||||||
|
)
|
||||||
|
(root / "pnpm-lock.yaml").touch()
|
||||||
|
(root / "pnpm-workspace.yaml").write_text('packages:\n - "apps/*"\n')
|
||||||
|
config_path = config_dir / "langgraph.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"node_version": "20",
|
||||||
|
"graphs": {"agent": "src/agent.ts:graph"},
|
||||||
|
"image_distro": "wolfi",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
save_path = root / "Dockerfile"
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"dockerfile",
|
||||||
|
str(save_path),
|
||||||
|
"--config",
|
||||||
|
str(config_path),
|
||||||
|
"--install-command",
|
||||||
|
"pnpm install --frozen-lockfile",
|
||||||
|
"--build-command",
|
||||||
|
"pnpm run build",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert save_path.exists()
|
||||||
|
dockerfile = save_path.read_text()
|
||||||
|
container_root = f"/deps/{root.name}"
|
||||||
|
assert f"ADD . {container_root}" in dockerfile
|
||||||
|
assert f"WORKDIR {container_root}" in dockerfile
|
||||||
|
assert "RUN pnpm install --frozen-lockfile" in dockerfile
|
||||||
|
assert f"WORKDIR {container_root}/apps/agent" in dockerfile
|
||||||
|
assert "RUN pnpm run build" in dockerfile
|
||||||
|
|
||||||
|
|
||||||
|
def test_dockerfile_command_rejects_disallowed_nodejs_commands() -> None:
|
||||||
|
runner = CliRunner()
|
||||||
|
config_content = {
|
||||||
|
"node_version": "20",
|
||||||
|
"graphs": {"agent": "agent.js:graph"},
|
||||||
|
}
|
||||||
|
|
||||||
|
with temporary_config_folder(config_content) as temp_dir:
|
||||||
|
(temp_dir / "agent.js").touch()
|
||||||
|
for option in ("--install-command", "--build-command"):
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"dockerfile",
|
||||||
|
str(temp_dir / "Dockerfile"),
|
||||||
|
"--config",
|
||||||
|
str(temp_dir / "config.json"),
|
||||||
|
option,
|
||||||
|
"npm install; echo bad",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert "contains disallowed characters or patterns" in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_build_command_with_api_version() -> None:
|
def test_build_command_with_api_version() -> None:
|
||||||
"""Test the 'build' command with --api-version flag."""
|
"""Test the 'build' command with --api-version flag."""
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|||||||
Reference in New Issue
Block a user