mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-28 18:59:42 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10dfc922af | ||
|
|
87d05fc0da | ||
|
|
f4c8adfc69 | ||
|
|
ffd16825a4 | ||
|
|
d4cb3914be | ||
|
|
2b5cd95263 | ||
|
|
fc70ba12fb | ||
|
|
e23540c63d | ||
|
|
f1a740e589 | ||
|
|
b12eb249cc | ||
|
|
502053a050 | ||
|
|
58273fde86 | ||
|
|
aeb9cdfef1 | ||
|
|
38031739e5 |
@@ -17,6 +17,9 @@ jobs:
|
||||
python-version:
|
||||
- "3.10"
|
||||
- "3.14"
|
||||
build_mode:
|
||||
- layered
|
||||
- flat
|
||||
example:
|
||||
- name: A
|
||||
workdir: libs/cli/examples
|
||||
@@ -30,9 +33,11 @@ jobs:
|
||||
- name: D
|
||||
workdir: libs/cli/examples/graphs_reqs_b
|
||||
tag: langgraph-test-d
|
||||
name: "CLI integration test"
|
||||
name: "CLI integration test (${{ matrix.python-version }}, ${{ matrix.example.name }}, ${{ matrix.build_mode }})"
|
||||
env:
|
||||
BUILD_ARGS: ${{ matrix.build_mode == 'flat' && '--flat' || '' }}
|
||||
HAS_LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY != '' }}
|
||||
TAG_SUFFIX: ${{ matrix.build_mode == 'flat' && '-flat' || '' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: libs/cli
|
||||
@@ -58,7 +63,7 @@ jobs:
|
||||
if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch')
|
||||
working-directory: ${{ matrix.example.workdir }}
|
||||
run: |
|
||||
langgraph build -t ${{ matrix.example.tag }}
|
||||
langgraph build $BUILD_ARGS -t ${{ matrix.example.tag }}$TAG_SUFFIX
|
||||
- name: Test service ${{ matrix.example.name }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
working-directory: ${{ matrix.example.workdir }}
|
||||
@@ -71,98 +76,98 @@ jobs:
|
||||
if [ -f ../.env ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> ../.env; fi
|
||||
# Run the integration test using the built tag
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
|
||||
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}$TAG_SUFFIX
|
||||
|
||||
- name: Build JS service
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') && matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/js-examples
|
||||
run: |
|
||||
langgraph build -t langgraph-test-e
|
||||
langgraph build $BUILD_ARGS -t langgraph-test-e$TAG_SUFFIX
|
||||
|
||||
- name: Build JS monorepo service
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') && matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/js-monorepo-example
|
||||
run: |
|
||||
langgraph build -t langgraph-test-f -c apps/agent/langgraph.json --build-command "yarn run turbo build" --install-command "yarn install"
|
||||
langgraph build $BUILD_ARGS -t langgraph-test-f$TAG_SUFFIX -c apps/agent/langgraph.json --build-command "yarn run turbo build" --install-command "yarn install"
|
||||
|
||||
- name: Build Python monorepo service
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') && matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/python-monorepo-example
|
||||
run: |
|
||||
langgraph build -t langgraph-test-g -c apps/agent/langgraph.json
|
||||
langgraph build $BUILD_ARGS -t langgraph-test-g$TAG_SUFFIX -c apps/agent/langgraph.json
|
||||
- name: Test Python monorepo service
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') && matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
working-directory: libs/cli/python-monorepo-example
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
cp apps/agent/.env.example apps/agent/.env
|
||||
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> apps/agent/.env
|
||||
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
|
||||
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g$TAG_SUFFIX -c apps/agent/langgraph.json
|
||||
|
||||
- name: Build prerelease reqs service
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') && matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/examples/graph_prerelease_reqs
|
||||
run: |
|
||||
langgraph build -t langgraph-test-h
|
||||
langgraph build $BUILD_ARGS -t langgraph-test-h$TAG_SUFFIX
|
||||
- name: Test prerelease reqs service
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') && matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
working-directory: libs/cli/examples/graph_prerelease_reqs
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
cp ../.env.example .env
|
||||
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h$TAG_SUFFIX
|
||||
echo "Finished starting up langgraph-test-h"
|
||||
LANGGRAPH_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langgraph'); print(v);")
|
||||
LANGGRAPH_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h$TAG_SUFFIX python -c "import sys; from importlib.metadata import version; v = version('langgraph'); print(v);")
|
||||
if [ "$LANGGRAPH_VERSION" != "1.1.5" ]; then
|
||||
echo "LANGGRAPH_VERSION != 1.1.5; $LANGGRAPH_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
LANGCHAIN_OPENAI_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langchain-openai'); print(v);")
|
||||
LANGCHAIN_OPENAI_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h$TAG_SUFFIX python -c "import sys; from importlib.metadata import version; v = version('langchain-openai'); print(v);")
|
||||
if [ "$LANGCHAIN_OPENAI_VERSION" != "1.1.14" ]; then
|
||||
echo "LANGCHAIN_OPENAI_VERSION != 1.1.14; $LANGCHAIN_OPENAI_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
LANGCHAIN_ANTHROPIC_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langchain-anthropic'); print(v);")
|
||||
LANGCHAIN_ANTHROPIC_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h$TAG_SUFFIX python -c "import sys; from importlib.metadata import version; v = version('langchain-anthropic'); print(v);")
|
||||
if [ "$LANGCHAIN_ANTHROPIC_VERSION" != "1.4.6" ]; then
|
||||
echo "LANGCHAIN_ANTHROPIC_VERSION != 1.4.6; $LANGCHAIN_ANTHROPIC_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build and test prerelease reqs fail service
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') && matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/examples/graph_prerelease_reqs_fail
|
||||
run: |
|
||||
langgraph build -t langgraph-test-i || [ $? -eq 1 ]
|
||||
langgraph build $BUILD_ARGS -t langgraph-test-i$TAG_SUFFIX || [ $? -eq 1 ]
|
||||
|
||||
- name: Build uv simple service
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') && matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/uv-examples/simple
|
||||
run: |
|
||||
langgraph build -t langgraph-test-uv-simple
|
||||
langgraph build $BUILD_ARGS -t langgraph-test-uv-simple$TAG_SUFFIX
|
||||
- name: Test uv simple service
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') && matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
working-directory: libs/cli/uv-examples/simple
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
cp .env.example .env
|
||||
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-uv-simple
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-uv-simple$TAG_SUFFIX
|
||||
|
||||
- name: Build uv monorepo service
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') && matrix.example.name == 'A' }}
|
||||
working-directory: libs/cli/uv-examples/monorepo/apps/agent
|
||||
run: |
|
||||
langgraph build -t langgraph-test-uv-monorepo
|
||||
langgraph build $BUILD_ARGS -t langgraph-test-uv-monorepo$TAG_SUFFIX
|
||||
- name: Test uv monorepo service
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') && matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||
working-directory: libs/cli/uv-examples/monorepo/apps/agent
|
||||
env:
|
||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||
run: |
|
||||
cp .env.example .env
|
||||
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env
|
||||
timeout 60 python ../../../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-uv-monorepo
|
||||
timeout 60 python ../../../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-uv-monorepo$TAG_SUFFIX
|
||||
|
||||
@@ -76,6 +76,7 @@ __pypackages__/
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.env.*
|
||||
.envrc
|
||||
*.crt
|
||||
*.key
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
This repository is a monorepo. Each library lives in a subdirectory under `libs/`.
|
||||
|
||||
<corridor>
|
||||
|
||||
## Corridor security analysis
|
||||
|
||||
When Corridor's `analyzePlan` tool is available, create a plan and use the tool to analyze it before generating or modifying code. Apply the resulting security guidance before writing code.
|
||||
|
||||
</corridor>
|
||||
|
||||
When you modify code in any library, run the following commands in that library's directory before creating a pull request:
|
||||
|
||||
- `make format` – run code formatters
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
This repository is a monorepo. Each library lives in a subdirectory under `libs/`.
|
||||
|
||||
<corridor>
|
||||
|
||||
## Corridor security analysis
|
||||
|
||||
Before generating or modifying code, create a plan and use Corridor's `analyzePlan` tool to analyze it. Apply the resulting security guidance before writing code.
|
||||
|
||||
</corridor>
|
||||
|
||||
When you modify code in any library, run the following commands in that library's directory before creating a pull request:
|
||||
|
||||
- `make format` – run code formatters
|
||||
|
||||
@@ -408,6 +408,12 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
|
||||
"--build-command",
|
||||
help="Custom build command to run from the langgraph.json directory. If not provided, uses default build process.",
|
||||
)
|
||||
@click.option(
|
||||
"--flat",
|
||||
is_flag=True,
|
||||
envvar="LANGGRAPH_CLI_FLAT_IMAGES",
|
||||
help="Flatten generated build steps into one image layer when supported.",
|
||||
)
|
||||
@click.argument("docker_build_args", nargs=-1, type=click.UNPROCESSED)
|
||||
@cli.command(
|
||||
help="📦 Build LangGraph API server Docker image.",
|
||||
@@ -426,6 +432,7 @@ def build(
|
||||
tag: str,
|
||||
install_command: str | None,
|
||||
build_command: str | None,
|
||||
flat: bool,
|
||||
):
|
||||
if install_command and langgraph_cli.config.has_disallowed_build_command_content(
|
||||
install_command
|
||||
@@ -461,6 +468,7 @@ def build(
|
||||
docker_build_args,
|
||||
install_command,
|
||||
build_command,
|
||||
flat=flat,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1162,6 +1162,208 @@ def _build_runtime_env_vars(config: Config) -> list[str]:
|
||||
return env_vars
|
||||
|
||||
|
||||
def _faux_package_pyproject_command(package_name: str) -> str:
|
||||
pyproject_path = shlex.quote(f"/deps/outer-{package_name}/pyproject.toml")
|
||||
return f"""cat > {pyproject_path} <<'PYPROJECT'
|
||||
[project]
|
||||
name = {json.dumps(package_name)}
|
||||
version = "0.1"
|
||||
[tool.setuptools.package-data]
|
||||
"*" = ["**/*"]
|
||||
[build-system]
|
||||
requires = ["setuptools>=61"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
PYPROJECT"""
|
||||
|
||||
|
||||
_FLAT_BUILD_CONTEXT = pathlib.PurePosixPath("/__build_context")
|
||||
_FLAT_ADDITIONAL_CONTEXTS = pathlib.PurePosixPath("/__additional_contexts")
|
||||
_DEFAULT_NODE_BUILD_COMMAND = (
|
||||
'(test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, '
|
||||
'skipping") || tsx /api/langgraph_api/js/build.mts'
|
||||
)
|
||||
|
||||
|
||||
def _render_flat_dockerfile(
|
||||
*,
|
||||
image: str,
|
||||
dockerfile_lines: list[str],
|
||||
env_vars: list[str],
|
||||
commands: list[str],
|
||||
workdir: str | None,
|
||||
additional_context_names: dict[pathlib.Path, str] | None = None,
|
||||
) -> str:
|
||||
mounts = [f"--mount=type=bind,target={_FLAT_BUILD_CONTEXT},readonly"]
|
||||
mounts.extend(
|
||||
f"--mount=type=bind,from={name},"
|
||||
f"target={_FLAT_ADDITIONAL_CONTEXTS / name},readonly"
|
||||
for name in (additional_context_names or {}).values()
|
||||
)
|
||||
|
||||
return os.linesep.join(
|
||||
[
|
||||
"# syntax=docker/dockerfile:1.7",
|
||||
f"FROM {image}",
|
||||
*dockerfile_lines,
|
||||
*env_vars,
|
||||
f"RUN {' '.join(mounts)} <<'USER_LAYER'",
|
||||
*commands,
|
||||
"USER_LAYER",
|
||||
f"WORKDIR {workdir}" if workdir else "",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _flat_context_source(
|
||||
path: pathlib.Path,
|
||||
relative_path: str,
|
||||
additional_context_names: dict[pathlib.Path, str],
|
||||
*,
|
||||
additional_relative_path: str = ".",
|
||||
) -> str:
|
||||
if name := additional_context_names.get(path):
|
||||
return str(_FLAT_ADDITIONAL_CONTEXTS / name / additional_relative_path)
|
||||
return str(_FLAT_BUILD_CONTEXT / relative_path)
|
||||
|
||||
|
||||
def _copy_directory_command(source: str, destination: str) -> str:
|
||||
return f"""mkdir -p {shlex.quote(destination)}
|
||||
cp -a {shlex.quote(f"{source}/.")} {shlex.quote(destination)}/"""
|
||||
|
||||
|
||||
def _flat_local_dependency_commands(
|
||||
*,
|
||||
config_path: pathlib.Path,
|
||||
local_deps: LocalDeps,
|
||||
local_reqs_pip_install: str,
|
||||
additional_context_names: dict[pathlib.Path, str],
|
||||
) -> list[str]:
|
||||
commands = []
|
||||
|
||||
for reqpath, destination in local_deps.pip_reqs:
|
||||
source = _flat_context_source(
|
||||
reqpath.parent,
|
||||
str(reqpath.relative_to(config_path.parent)),
|
||||
additional_context_names,
|
||||
additional_relative_path=reqpath.name,
|
||||
)
|
||||
destination_parent = pathlib.PurePosixPath(destination).parent
|
||||
commands.append(f"""mkdir -p {shlex.quote(str(destination_parent))}
|
||||
cp {shlex.quote(source)} {shlex.quote(destination)}""")
|
||||
if local_deps.pip_reqs:
|
||||
requirements = " ".join(
|
||||
f"-r {destination}" for _, destination in local_deps.pip_reqs
|
||||
)
|
||||
commands.append(f"{local_reqs_pip_install} {requirements}")
|
||||
|
||||
for full_path, (relative_path, name) in local_deps.real_pkgs.items():
|
||||
source = _flat_context_source(
|
||||
full_path, relative_path, additional_context_names
|
||||
)
|
||||
commands.append(_copy_directory_command(source, f"/deps/{name}"))
|
||||
|
||||
for full_path, (relative_path, destination) in local_deps.faux_pkgs.items():
|
||||
source = _flat_context_source(
|
||||
full_path, relative_path, additional_context_names
|
||||
)
|
||||
pyproject = _faux_package_pyproject_command(full_path.name)
|
||||
commands.append(f"{_copy_directory_command(source, destination)}\n{pyproject}")
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def _build_flat_python_commands(
|
||||
*,
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
local_deps: LocalDeps,
|
||||
pypi_deps: list[str],
|
||||
local_reqs_pip_install: str,
|
||||
local_deps_install_command: str,
|
||||
pip_cleanup: str,
|
||||
additional_context_names: dict[pathlib.Path, str],
|
||||
js_working_dir: str | None,
|
||||
) -> list[str]:
|
||||
commands = ["set -eu"]
|
||||
|
||||
if js_working_dir:
|
||||
commands.append("/storage/install-node.sh")
|
||||
|
||||
if pip_config_file := config.get("pip_config_file"):
|
||||
source = shlex.quote(str(_FLAT_BUILD_CONTEXT / pip_config_file))
|
||||
commands.append(f"cp {source} /pipconfig.txt")
|
||||
if pypi_deps:
|
||||
commands.append(f"{local_reqs_pip_install} {' '.join(pypi_deps)}")
|
||||
|
||||
commands.extend(
|
||||
_flat_local_dependency_commands(
|
||||
config_path=config_path,
|
||||
local_deps=local_deps,
|
||||
local_reqs_pip_install=local_reqs_pip_install,
|
||||
additional_context_names=additional_context_names,
|
||||
)
|
||||
)
|
||||
|
||||
commands.append(local_deps_install_command)
|
||||
if js_working_dir:
|
||||
working_dir = shlex.quote(js_working_dir)
|
||||
commands.append(
|
||||
f"cd {working_dir}\n"
|
||||
f"{_get_node_pm_install_cmd(config_path.parent)} "
|
||||
"&& tsx /api/langgraph_api/js/build.mts"
|
||||
)
|
||||
commands.extend(
|
||||
line.removeprefix("RUN ")
|
||||
for line in pip_cleanup.splitlines()
|
||||
if line and not line.startswith("#")
|
||||
)
|
||||
return commands
|
||||
|
||||
|
||||
def _build_flat_python_dockerfile(
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
local_deps: LocalDeps,
|
||||
pypi_deps: list[str],
|
||||
local_reqs_pip_install: str,
|
||||
local_deps_install_command: str,
|
||||
pip_cleanup: str,
|
||||
image: str,
|
||||
env_vars: list[str],
|
||||
additional_context_names: dict[pathlib.Path, str],
|
||||
) -> str:
|
||||
js_working_dir = (
|
||||
local_deps.working_dir
|
||||
if config.get("ui") or config.get("node_version")
|
||||
else None
|
||||
)
|
||||
commands = _build_flat_python_commands(
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
local_deps=local_deps,
|
||||
pypi_deps=pypi_deps,
|
||||
local_reqs_pip_install=local_reqs_pip_install,
|
||||
local_deps_install_command=local_deps_install_command,
|
||||
pip_cleanup=pip_cleanup,
|
||||
additional_context_names=additional_context_names,
|
||||
js_working_dir=js_working_dir,
|
||||
)
|
||||
if js_working_dir:
|
||||
env_vars = [
|
||||
f"ENV NODE_VERSION={config.get('node_version') or DEFAULT_NODE_VERSION}",
|
||||
*env_vars,
|
||||
]
|
||||
|
||||
return _render_flat_dockerfile(
|
||||
image=image,
|
||||
dockerfile_lines=config["dockerfile_lines"],
|
||||
env_vars=env_vars,
|
||||
commands=commands,
|
||||
workdir=local_deps.working_dir,
|
||||
additional_context_names=additional_context_names,
|
||||
)
|
||||
|
||||
|
||||
def _get_node_pm_install_cmd(project_dir: pathlib.Path) -> str:
|
||||
def test_file(file_name):
|
||||
full_path = project_dir / file_name
|
||||
@@ -1267,6 +1469,7 @@ def python_config_to_docker(
|
||||
api_version: str | None = None,
|
||||
*,
|
||||
escape_variables: bool = False,
|
||||
flat: bool = False,
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
"""Generate a Dockerfile from the configuration."""
|
||||
source_kind = _get_source_kind(config)
|
||||
@@ -1428,6 +1631,36 @@ ADD {relpath} /deps/{name}
|
||||
]
|
||||
)
|
||||
image_str = docker_tag(config, base_image, api_version)
|
||||
dep_vname = "$$dep" if escape_variables else "$dep"
|
||||
local_deps_install_command = f"""for dep in /deps/*; do \
|
||||
echo "Installing {dep_vname}"; \
|
||||
if [ -d "{dep_vname}" ]; then \
|
||||
echo "Installing {dep_vname}"; \
|
||||
(cd "{dep_vname}" && {global_reqs_pip_install} -e .); \
|
||||
fi; \
|
||||
done"""
|
||||
pip_cleanup = _get_pip_cleanup_lines(
|
||||
install_cmd=install_cmd,
|
||||
to_uninstall=build_tools_to_uninstall,
|
||||
pip_installer=pip_installer,
|
||||
)
|
||||
|
||||
if flat:
|
||||
return (
|
||||
_build_flat_python_dockerfile(
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
local_deps=local_deps,
|
||||
pypi_deps=pypi_deps,
|
||||
local_reqs_pip_install=local_reqs_pip_install,
|
||||
local_deps_install_command=local_deps_install_command,
|
||||
pip_cleanup=pip_cleanup,
|
||||
image=image_str,
|
||||
env_vars=env_vars,
|
||||
additional_context_names=additional_context_names,
|
||||
),
|
||||
additional_contexts,
|
||||
)
|
||||
|
||||
# Prepare docker file contents
|
||||
docker_file_contents = []
|
||||
@@ -1442,14 +1675,6 @@ ADD {relpath} /deps/{name}
|
||||
)
|
||||
|
||||
# Add main dockerfile content
|
||||
dep_vname = "$$dep" if escape_variables else "$dep"
|
||||
local_deps_install_str = f"""RUN for dep in /deps/*; do \
|
||||
echo "Installing {dep_vname}"; \
|
||||
if [ -d "{dep_vname}" ]; then \
|
||||
echo "Installing {dep_vname}"; \
|
||||
(cd "{dep_vname}" && {global_reqs_pip_install} -e .); \
|
||||
fi; \
|
||||
done"""
|
||||
docker_file_contents.extend(
|
||||
[
|
||||
f"FROM {image_str}",
|
||||
@@ -1459,18 +1684,14 @@ ADD {relpath} /deps/{name}
|
||||
installs,
|
||||
"",
|
||||
"# -- Installing all local dependencies --",
|
||||
local_deps_install_str,
|
||||
f"RUN {local_deps_install_command}",
|
||||
"# -- End of local dependencies install --",
|
||||
os.linesep.join(env_vars),
|
||||
"",
|
||||
js_inst_str,
|
||||
"",
|
||||
# Add pip cleanup after all installations are complete
|
||||
_get_pip_cleanup_lines(
|
||||
install_cmd=install_cmd,
|
||||
to_uninstall=build_tools_to_uninstall,
|
||||
pip_installer=pip_installer,
|
||||
),
|
||||
pip_cleanup,
|
||||
"",
|
||||
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
|
||||
]
|
||||
@@ -1479,6 +1700,31 @@ ADD {relpath} /deps/{name}
|
||||
return os.linesep.join(docker_file_contents), additional_contexts
|
||||
|
||||
|
||||
def _build_flat_node_dockerfile(
|
||||
*,
|
||||
config: Config,
|
||||
image: str,
|
||||
env_vars: list[str],
|
||||
install_workdir: str,
|
||||
build_workdir: str,
|
||||
install_command: str,
|
||||
build_command: str,
|
||||
) -> str:
|
||||
commands = [
|
||||
"set -eu",
|
||||
_copy_directory_command(str(_FLAT_BUILD_CONTEXT), install_workdir),
|
||||
f"cd {shlex.quote(install_workdir)}\n{install_command}",
|
||||
f"cd {shlex.quote(build_workdir)}\n{build_command}",
|
||||
]
|
||||
return _render_flat_dockerfile(
|
||||
image=image,
|
||||
dockerfile_lines=config["dockerfile_lines"],
|
||||
env_vars=env_vars,
|
||||
commands=commands,
|
||||
workdir=build_workdir,
|
||||
)
|
||||
|
||||
|
||||
def node_config_to_docker(
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
@@ -1487,6 +1733,7 @@ def node_config_to_docker(
|
||||
install_command: str | None = None,
|
||||
build_command: str | None = None,
|
||||
build_context: str | None = None,
|
||||
flat: bool = False,
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
# Calculate paths for monorepo support
|
||||
install_root = (
|
||||
@@ -1496,56 +1743,56 @@ def node_config_to_docker(
|
||||
if build_context:
|
||||
relative_workdir = _calculate_relative_workdir(config_path, build_context)
|
||||
container_name = pathlib.Path(build_context).name
|
||||
if relative_workdir:
|
||||
faux_path = f"/deps/{container_name}/{relative_workdir}"
|
||||
else:
|
||||
faux_path = f"/deps/{container_name}"
|
||||
install_workdir = f"/deps/{container_name}"
|
||||
build_workdir = (
|
||||
f"{install_workdir}/{relative_workdir}"
|
||||
if relative_workdir
|
||||
else install_workdir
|
||||
)
|
||||
else:
|
||||
# Backward compatibility: use the original behavior
|
||||
faux_path = f"/deps/{config_path.parent.name}"
|
||||
install_workdir = build_workdir = f"/deps/{config_path.parent.name}"
|
||||
|
||||
image_str = docker_tag(config, base_image, api_version)
|
||||
|
||||
env_vars = _build_runtime_env_vars(config)
|
||||
|
||||
# For monorepo support, we need to handle install and build commands differently
|
||||
if build_context:
|
||||
# Monorepo case: install from root, build from config directory
|
||||
container_root = f"/deps/{pathlib.Path(build_context).name}"
|
||||
install_workdir = container_root
|
||||
install_step = f"RUN {install_cmd}"
|
||||
build_cmd = (
|
||||
build_command
|
||||
if build_context and build_command
|
||||
else _DEFAULT_NODE_BUILD_COMMAND
|
||||
)
|
||||
|
||||
if build_command:
|
||||
build_step = f"RUN {build_command}"
|
||||
else:
|
||||
build_step = 'RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts'
|
||||
else:
|
||||
# Original behavior: everything happens in the same directory
|
||||
install_workdir = faux_path
|
||||
install_step = f"RUN {install_cmd}"
|
||||
build_step = 'RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts'
|
||||
|
||||
if build_context:
|
||||
build_workdir = faux_path
|
||||
else:
|
||||
build_workdir = faux_path
|
||||
if flat:
|
||||
return (
|
||||
_build_flat_node_dockerfile(
|
||||
config=config,
|
||||
image=image_str,
|
||||
env_vars=env_vars,
|
||||
install_workdir=install_workdir,
|
||||
build_workdir=build_workdir,
|
||||
install_command=install_cmd,
|
||||
build_command=build_cmd,
|
||||
),
|
||||
{},
|
||||
)
|
||||
|
||||
docker_file_contents = [
|
||||
f"FROM {image_str}",
|
||||
"",
|
||||
os.linesep.join(config["dockerfile_lines"]),
|
||||
"",
|
||||
f"ADD . {faux_path if not build_context else container_root}",
|
||||
f"ADD . {install_workdir}",
|
||||
"",
|
||||
f"WORKDIR {install_workdir}",
|
||||
"",
|
||||
install_step,
|
||||
f"RUN {install_cmd}",
|
||||
"",
|
||||
os.linesep.join(env_vars),
|
||||
"",
|
||||
f"WORKDIR {build_workdir}",
|
||||
"",
|
||||
build_step,
|
||||
f"RUN {build_cmd}",
|
||||
]
|
||||
|
||||
return os.linesep.join(docker_file_contents), {}
|
||||
@@ -1625,6 +1872,7 @@ def config_to_docker(
|
||||
build_command: str | None = None,
|
||||
build_context: str | None = None,
|
||||
escape_variables: bool = False,
|
||||
flat: bool = False,
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
base_image = base_image or default_base_image(config)
|
||||
|
||||
@@ -1637,6 +1885,7 @@ def config_to_docker(
|
||||
install_command=install_command,
|
||||
build_command=build_command,
|
||||
build_context=build_context,
|
||||
flat=flat,
|
||||
)
|
||||
|
||||
return python_config_to_docker(
|
||||
@@ -1645,6 +1894,7 @@ def config_to_docker(
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
escape_variables=escape_variables,
|
||||
flat=flat,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -345,6 +345,7 @@ def build_docker_image(
|
||||
docker_command: Sequence[str] | None = None,
|
||||
extra_flags: Sequence[str] = (),
|
||||
verbose: bool = True,
|
||||
flat: bool = False,
|
||||
):
|
||||
"""Build a Docker image from a LangGraph config."""
|
||||
# pull latest images
|
||||
@@ -387,6 +388,7 @@ def build_docker_image(
|
||||
install_command=install_command,
|
||||
build_command=build_command,
|
||||
build_context=build_context,
|
||||
flat=flat,
|
||||
)
|
||||
# add additional_contexts
|
||||
if additional_contexts:
|
||||
|
||||
@@ -723,6 +723,67 @@ WORKDIR /deps/outer-unit_tests/unit_tests\
|
||||
}
|
||||
|
||||
|
||||
def test_config_to_docker_flat():
|
||||
dockerfile, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{"dependencies": ["."], "graphs": {"agent": "./agent.py:graph"}}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
flat=True,
|
||||
)
|
||||
|
||||
assert additional_contexts == {}
|
||||
assert dockerfile.count("\nRUN ") == 1
|
||||
assert "--mount=type=bind" in dockerfile
|
||||
assert "cp -a /__build_context/. /deps/outer-unit_tests/unit_tests/" in dockerfile
|
||||
assert "cat > /deps/outer-unit_tests/pyproject.toml <<'PYPROJECT'" in dockerfile
|
||||
assert '[tool.setuptools.package-data]\n"*" = ["**/*"]' in dockerfile
|
||||
assert dockerfile.endswith("WORKDIR /deps/outer-unit_tests/unit_tests")
|
||||
|
||||
|
||||
def test_config_to_docker_flat_with_custom_lines_and_escaped_variables():
|
||||
dockerfile, _ = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
"dockerfile_lines": ["RUN echo custom"],
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
escape_variables=True,
|
||||
flat=True,
|
||||
)
|
||||
|
||||
assert dockerfile.index("RUN echo custom") < dockerfile.index("USER_LAYER")
|
||||
assert 'echo "Installing $$dep"' in dockerfile
|
||||
|
||||
|
||||
def test_config_to_docker_flat_with_additional_context():
|
||||
dockerfile, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{"dependencies": [".", ".."], "graphs": {"agent": "./agent.py:graph"}}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
flat=True,
|
||||
)
|
||||
|
||||
assert additional_contexts == {
|
||||
"outer-tests": str(pathlib.Path(__file__).parent.parent.absolute()),
|
||||
}
|
||||
assert (
|
||||
"--mount=type=bind,from=outer-tests,"
|
||||
"target=/__additional_contexts/outer-tests,readonly" in dockerfile
|
||||
)
|
||||
assert (
|
||||
"cp -a /__additional_contexts/outer-tests/. /deps/outer-tests/tests/"
|
||||
in dockerfile
|
||||
)
|
||||
|
||||
|
||||
def test_config_to_docker_outside_path():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
@@ -998,6 +1059,28 @@ RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not foun
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
def test_config_to_docker_nodejs_flat():
|
||||
dockerfile, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {"agent": "./graphs/agent.js:graph"},
|
||||
"dockerfile_lines": ["RUN echo custom"],
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraphjs-api",
|
||||
flat=True,
|
||||
)
|
||||
|
||||
assert additional_contexts == {}
|
||||
assert dockerfile.index("RUN echo custom") < dockerfile.index("USER_LAYER")
|
||||
assert "cp -a /__build_context/. /deps/unit_tests/" in dockerfile
|
||||
assert "cd /deps/unit_tests\nnpm i" in dockerfile
|
||||
assert "tsx /api/langgraph_api/js/build.mts" in dockerfile
|
||||
assert dockerfile.endswith("WORKDIR /deps/unit_tests")
|
||||
|
||||
|
||||
def test_config_to_docker_python_encryption():
|
||||
# Test that encryption config is included in validation
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
@@ -1214,6 +1297,29 @@ WORKDIR /deps/outer-unit_tests/unit_tests"""
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
def test_config_to_docker_gen_ui_python_flat():
|
||||
dockerfile, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
"ui": {"agent": "./graphs/agent.ui.jsx"},
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
flat=True,
|
||||
)
|
||||
|
||||
assert additional_contexts == {}
|
||||
assert dockerfile.count("\nRUN ") == 1
|
||||
assert dockerfile.index("ENV NODE_VERSION=20") < dockerfile.index("\nRUN ")
|
||||
assert dockerfile.index("ENV LANGGRAPH_UI=") < dockerfile.index("\nRUN ")
|
||||
assert "/storage/install-node.sh" in dockerfile
|
||||
assert "cd /deps/outer-unit_tests/unit_tests\nnpm i" in dockerfile
|
||||
assert "tsx /api/langgraph_api/js/build.mts" in dockerfile
|
||||
|
||||
|
||||
def test_config_to_docker_multiplatform():
|
||||
graphs = {
|
||||
"python": "./multiplatform/python.py:graph",
|
||||
|
||||
Reference in New Issue
Block a user