mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-23 18:15:08 +02:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10dfc922af | ||
|
|
87d05fc0da | ||
|
|
f4c8adfc69 | ||
|
|
ffd16825a4 | ||
|
|
d4cb3914be | ||
|
|
2b5cd95263 | ||
|
|
fc70ba12fb | ||
|
|
e23540c63d | ||
|
|
f1a740e589 | ||
|
|
b12eb249cc | ||
|
|
502053a050 | ||
|
|
58273fde86 | ||
|
|
aeb9cdfef1 |
@@ -17,6 +17,9 @@ jobs:
|
|||||||
python-version:
|
python-version:
|
||||||
- "3.10"
|
- "3.10"
|
||||||
- "3.14"
|
- "3.14"
|
||||||
|
build_mode:
|
||||||
|
- layered
|
||||||
|
- flat
|
||||||
example:
|
example:
|
||||||
- name: A
|
- name: A
|
||||||
workdir: libs/cli/examples
|
workdir: libs/cli/examples
|
||||||
@@ -30,9 +33,11 @@ jobs:
|
|||||||
- name: D
|
- name: D
|
||||||
workdir: libs/cli/examples/graphs_reqs_b
|
workdir: libs/cli/examples/graphs_reqs_b
|
||||||
tag: langgraph-test-d
|
tag: langgraph-test-d
|
||||||
name: "CLI integration test"
|
name: "CLI integration test (${{ matrix.python-version }}, ${{ matrix.example.name }}, ${{ matrix.build_mode }})"
|
||||||
env:
|
env:
|
||||||
|
BUILD_ARGS: ${{ matrix.build_mode == 'flat' && '--flat' || '' }}
|
||||||
HAS_LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY != '' }}
|
HAS_LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY != '' }}
|
||||||
|
TAG_SUFFIX: ${{ matrix.build_mode == 'flat' && '-flat' || '' }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: libs/cli
|
working-directory: libs/cli
|
||||||
@@ -58,7 +63,7 @@ jobs:
|
|||||||
if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch')
|
if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch')
|
||||||
working-directory: ${{ matrix.example.workdir }}
|
working-directory: ${{ matrix.example.workdir }}
|
||||||
run: |
|
run: |
|
||||||
langgraph build -t ${{ matrix.example.tag }}
|
langgraph build $BUILD_ARGS -t ${{ matrix.example.tag }}$TAG_SUFFIX
|
||||||
- name: Test service ${{ matrix.example.name }}
|
- name: Test service ${{ matrix.example.name }}
|
||||||
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&env.HAS_LANGSMITH_API_KEY == 'true' }}
|
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&env.HAS_LANGSMITH_API_KEY == 'true' }}
|
||||||
working-directory: ${{ matrix.example.workdir }}
|
working-directory: ${{ matrix.example.workdir }}
|
||||||
@@ -71,98 +76,98 @@ jobs:
|
|||||||
if [ -f ../.env ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> ../.env; fi
|
if [ -f ../.env ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> ../.env; fi
|
||||||
# Run the integration test using the built tag
|
# Run the integration test using the built tag
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
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
|
- 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
|
working-directory: libs/cli/js-examples
|
||||||
run: |
|
run: |
|
||||||
langgraph build -t langgraph-test-e
|
langgraph build $BUILD_ARGS -t langgraph-test-e$TAG_SUFFIX
|
||||||
|
|
||||||
- name: Build JS monorepo service
|
- 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
|
working-directory: libs/cli/js-monorepo-example
|
||||||
run: |
|
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
|
- 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
|
working-directory: libs/cli/python-monorepo-example
|
||||||
run: |
|
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
|
- 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
|
working-directory: libs/cli/python-monorepo-example
|
||||||
env:
|
env:
|
||||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||||
run: |
|
run: |
|
||||||
cp apps/agent/.env.example apps/agent/.env
|
cp apps/agent/.env.example apps/agent/.env
|
||||||
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> 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
|
- 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
|
working-directory: libs/cli/examples/graph_prerelease_reqs
|
||||||
run: |
|
run: |
|
||||||
langgraph build -t langgraph-test-h
|
langgraph build $BUILD_ARGS -t langgraph-test-h$TAG_SUFFIX
|
||||||
- name: Test prerelease reqs service
|
- 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
|
working-directory: libs/cli/examples/graph_prerelease_reqs
|
||||||
env:
|
env:
|
||||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||||
run: |
|
run: |
|
||||||
cp ../.env.example .env
|
cp ../.env.example .env
|
||||||
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .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"
|
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
|
if [ "$LANGGRAPH_VERSION" != "1.1.5" ]; then
|
||||||
echo "LANGGRAPH_VERSION != 1.1.5; $LANGGRAPH_VERSION"
|
echo "LANGGRAPH_VERSION != 1.1.5; $LANGGRAPH_VERSION"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
if [ "$LANGCHAIN_OPENAI_VERSION" != "1.1.14" ]; then
|
||||||
echo "LANGCHAIN_OPENAI_VERSION != 1.1.14; $LANGCHAIN_OPENAI_VERSION"
|
echo "LANGCHAIN_OPENAI_VERSION != 1.1.14; $LANGCHAIN_OPENAI_VERSION"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
if [ "$LANGCHAIN_ANTHROPIC_VERSION" != "1.4.6" ]; then
|
||||||
echo "LANGCHAIN_ANTHROPIC_VERSION != 1.4.6; $LANGCHAIN_ANTHROPIC_VERSION"
|
echo "LANGCHAIN_ANTHROPIC_VERSION != 1.4.6; $LANGCHAIN_ANTHROPIC_VERSION"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build and test prerelease reqs fail service
|
- 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
|
working-directory: libs/cli/examples/graph_prerelease_reqs_fail
|
||||||
run: |
|
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
|
- 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
|
working-directory: libs/cli/uv-examples/simple
|
||||||
run: |
|
run: |
|
||||||
langgraph build -t langgraph-test-uv-simple
|
langgraph build $BUILD_ARGS -t langgraph-test-uv-simple$TAG_SUFFIX
|
||||||
- name: Test uv simple service
|
- 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
|
working-directory: libs/cli/uv-examples/simple
|
||||||
env:
|
env:
|
||||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||||
run: |
|
run: |
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .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
|
- 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
|
working-directory: libs/cli/uv-examples/monorepo/apps/agent
|
||||||
run: |
|
run: |
|
||||||
langgraph build -t langgraph-test-uv-monorepo
|
langgraph build $BUILD_ARGS -t langgraph-test-uv-monorepo$TAG_SUFFIX
|
||||||
- name: Test uv monorepo service
|
- 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
|
working-directory: libs/cli/uv-examples/monorepo/apps/agent
|
||||||
env:
|
env:
|
||||||
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
||||||
run: |
|
run: |
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .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
|
# Environments
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
.envrc
|
.envrc
|
||||||
*.crt
|
*.crt
|
||||||
*.key
|
*.key
|
||||||
|
|||||||
@@ -408,6 +408,12 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
|
|||||||
"--build-command",
|
"--build-command",
|
||||||
help="Custom build command to run from the langgraph.json directory. If not provided, uses default build process.",
|
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)
|
@click.argument("docker_build_args", nargs=-1, type=click.UNPROCESSED)
|
||||||
@cli.command(
|
@cli.command(
|
||||||
help="📦 Build LangGraph API server Docker image.",
|
help="📦 Build LangGraph API server Docker image.",
|
||||||
@@ -426,6 +432,7 @@ def build(
|
|||||||
tag: str,
|
tag: str,
|
||||||
install_command: str | None,
|
install_command: str | None,
|
||||||
build_command: str | None,
|
build_command: str | None,
|
||||||
|
flat: bool,
|
||||||
):
|
):
|
||||||
if install_command and langgraph_cli.config.has_disallowed_build_command_content(
|
if install_command and langgraph_cli.config.has_disallowed_build_command_content(
|
||||||
install_command
|
install_command
|
||||||
@@ -461,6 +468,7 @@ def build(
|
|||||||
docker_build_args,
|
docker_build_args,
|
||||||
install_command,
|
install_command,
|
||||||
build_command,
|
build_command,
|
||||||
|
flat=flat,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1162,6 +1162,208 @@ def _build_runtime_env_vars(config: Config) -> list[str]:
|
|||||||
return env_vars
|
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 _get_node_pm_install_cmd(project_dir: pathlib.Path) -> str:
|
||||||
def test_file(file_name):
|
def test_file(file_name):
|
||||||
full_path = project_dir / file_name
|
full_path = project_dir / file_name
|
||||||
@@ -1267,6 +1469,7 @@ def python_config_to_docker(
|
|||||||
api_version: str | None = None,
|
api_version: str | None = None,
|
||||||
*,
|
*,
|
||||||
escape_variables: bool = False,
|
escape_variables: bool = False,
|
||||||
|
flat: bool = False,
|
||||||
) -> tuple[str, dict[str, str]]:
|
) -> tuple[str, dict[str, str]]:
|
||||||
"""Generate a Dockerfile from the configuration."""
|
"""Generate a Dockerfile from the configuration."""
|
||||||
source_kind = _get_source_kind(config)
|
source_kind = _get_source_kind(config)
|
||||||
@@ -1428,6 +1631,36 @@ ADD {relpath} /deps/{name}
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
image_str = docker_tag(config, base_image, api_version)
|
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
|
# Prepare docker file contents
|
||||||
docker_file_contents = []
|
docker_file_contents = []
|
||||||
@@ -1442,14 +1675,6 @@ ADD {relpath} /deps/{name}
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Add main dockerfile content
|
# 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(
|
docker_file_contents.extend(
|
||||||
[
|
[
|
||||||
f"FROM {image_str}",
|
f"FROM {image_str}",
|
||||||
@@ -1459,18 +1684,14 @@ ADD {relpath} /deps/{name}
|
|||||||
installs,
|
installs,
|
||||||
"",
|
"",
|
||||||
"# -- Installing all local dependencies --",
|
"# -- Installing all local dependencies --",
|
||||||
local_deps_install_str,
|
f"RUN {local_deps_install_command}",
|
||||||
"# -- End of local dependencies install --",
|
"# -- End of local dependencies install --",
|
||||||
os.linesep.join(env_vars),
|
os.linesep.join(env_vars),
|
||||||
"",
|
"",
|
||||||
js_inst_str,
|
js_inst_str,
|
||||||
"",
|
"",
|
||||||
# Add pip cleanup after all installations are complete
|
# Add pip cleanup after all installations are complete
|
||||||
_get_pip_cleanup_lines(
|
pip_cleanup,
|
||||||
install_cmd=install_cmd,
|
|
||||||
to_uninstall=build_tools_to_uninstall,
|
|
||||||
pip_installer=pip_installer,
|
|
||||||
),
|
|
||||||
"",
|
"",
|
||||||
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
|
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
|
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(
|
def node_config_to_docker(
|
||||||
config_path: pathlib.Path,
|
config_path: pathlib.Path,
|
||||||
config: Config,
|
config: Config,
|
||||||
@@ -1487,6 +1733,7 @@ def node_config_to_docker(
|
|||||||
install_command: str | None = None,
|
install_command: str | None = None,
|
||||||
build_command: str | None = None,
|
build_command: str | None = None,
|
||||||
build_context: str | None = None,
|
build_context: str | None = None,
|
||||||
|
flat: bool = False,
|
||||||
) -> tuple[str, dict[str, str]]:
|
) -> tuple[str, dict[str, str]]:
|
||||||
# Calculate paths for monorepo support
|
# Calculate paths for monorepo support
|
||||||
install_root = (
|
install_root = (
|
||||||
@@ -1496,56 +1743,56 @@ def node_config_to_docker(
|
|||||||
if build_context:
|
if build_context:
|
||||||
relative_workdir = _calculate_relative_workdir(config_path, build_context)
|
relative_workdir = _calculate_relative_workdir(config_path, build_context)
|
||||||
container_name = pathlib.Path(build_context).name
|
container_name = pathlib.Path(build_context).name
|
||||||
if relative_workdir:
|
install_workdir = f"/deps/{container_name}"
|
||||||
faux_path = f"/deps/{container_name}/{relative_workdir}"
|
build_workdir = (
|
||||||
else:
|
f"{install_workdir}/{relative_workdir}"
|
||||||
faux_path = f"/deps/{container_name}"
|
if relative_workdir
|
||||||
|
else install_workdir
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Backward compatibility: use the original behavior
|
# 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)
|
image_str = docker_tag(config, base_image, api_version)
|
||||||
|
|
||||||
env_vars = _build_runtime_env_vars(config)
|
env_vars = _build_runtime_env_vars(config)
|
||||||
|
|
||||||
# For monorepo support, we need to handle install and build commands differently
|
build_cmd = (
|
||||||
if build_context:
|
build_command
|
||||||
# Monorepo case: install from root, build from config directory
|
if build_context and build_command
|
||||||
container_root = f"/deps/{pathlib.Path(build_context).name}"
|
else _DEFAULT_NODE_BUILD_COMMAND
|
||||||
install_workdir = container_root
|
)
|
||||||
install_step = f"RUN {install_cmd}"
|
|
||||||
|
|
||||||
if build_command:
|
if flat:
|
||||||
build_step = f"RUN {build_command}"
|
return (
|
||||||
else:
|
_build_flat_node_dockerfile(
|
||||||
build_step = 'RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts'
|
config=config,
|
||||||
else:
|
image=image_str,
|
||||||
# Original behavior: everything happens in the same directory
|
env_vars=env_vars,
|
||||||
install_workdir = faux_path
|
install_workdir=install_workdir,
|
||||||
install_step = f"RUN {install_cmd}"
|
build_workdir=build_workdir,
|
||||||
build_step = 'RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts'
|
install_command=install_cmd,
|
||||||
|
build_command=build_cmd,
|
||||||
if build_context:
|
),
|
||||||
build_workdir = faux_path
|
{},
|
||||||
else:
|
)
|
||||||
build_workdir = faux_path
|
|
||||||
|
|
||||||
docker_file_contents = [
|
docker_file_contents = [
|
||||||
f"FROM {image_str}",
|
f"FROM {image_str}",
|
||||||
"",
|
"",
|
||||||
os.linesep.join(config["dockerfile_lines"]),
|
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}",
|
f"WORKDIR {install_workdir}",
|
||||||
"",
|
"",
|
||||||
install_step,
|
f"RUN {install_cmd}",
|
||||||
"",
|
"",
|
||||||
os.linesep.join(env_vars),
|
os.linesep.join(env_vars),
|
||||||
"",
|
"",
|
||||||
f"WORKDIR {build_workdir}",
|
f"WORKDIR {build_workdir}",
|
||||||
"",
|
"",
|
||||||
build_step,
|
f"RUN {build_cmd}",
|
||||||
]
|
]
|
||||||
|
|
||||||
return os.linesep.join(docker_file_contents), {}
|
return os.linesep.join(docker_file_contents), {}
|
||||||
@@ -1625,6 +1872,7 @@ def config_to_docker(
|
|||||||
build_command: str | None = None,
|
build_command: str | None = None,
|
||||||
build_context: str | None = None,
|
build_context: str | None = None,
|
||||||
escape_variables: bool = False,
|
escape_variables: bool = False,
|
||||||
|
flat: bool = False,
|
||||||
) -> tuple[str, dict[str, str]]:
|
) -> tuple[str, dict[str, str]]:
|
||||||
base_image = base_image or default_base_image(config)
|
base_image = base_image or default_base_image(config)
|
||||||
|
|
||||||
@@ -1637,6 +1885,7 @@ def config_to_docker(
|
|||||||
install_command=install_command,
|
install_command=install_command,
|
||||||
build_command=build_command,
|
build_command=build_command,
|
||||||
build_context=build_context,
|
build_context=build_context,
|
||||||
|
flat=flat,
|
||||||
)
|
)
|
||||||
|
|
||||||
return python_config_to_docker(
|
return python_config_to_docker(
|
||||||
@@ -1645,6 +1894,7 @@ def config_to_docker(
|
|||||||
base_image=base_image,
|
base_image=base_image,
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
escape_variables=escape_variables,
|
escape_variables=escape_variables,
|
||||||
|
flat=flat,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ def build_docker_image(
|
|||||||
docker_command: Sequence[str] | None = None,
|
docker_command: Sequence[str] | None = None,
|
||||||
extra_flags: Sequence[str] = (),
|
extra_flags: Sequence[str] = (),
|
||||||
verbose: bool = True,
|
verbose: bool = True,
|
||||||
|
flat: bool = False,
|
||||||
):
|
):
|
||||||
"""Build a Docker image from a LangGraph config."""
|
"""Build a Docker image from a LangGraph config."""
|
||||||
# pull latest images
|
# pull latest images
|
||||||
@@ -387,6 +388,7 @@ def build_docker_image(
|
|||||||
install_command=install_command,
|
install_command=install_command,
|
||||||
build_command=build_command,
|
build_command=build_command,
|
||||||
build_context=build_context,
|
build_context=build_context,
|
||||||
|
flat=flat,
|
||||||
)
|
)
|
||||||
# add additional_contexts
|
# add additional_contexts
|
||||||
if 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():
|
def test_config_to_docker_outside_path():
|
||||||
graphs = {"agent": "./agent.py:graph"}
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
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 == {}
|
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():
|
def test_config_to_docker_python_encryption():
|
||||||
# Test that encryption config is included in validation
|
# Test that encryption config is included in validation
|
||||||
graphs = {"agent": "./agent.py:graph"}
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
@@ -1214,6 +1297,29 @@ WORKDIR /deps/outer-unit_tests/unit_tests"""
|
|||||||
assert additional_contexts == {}
|
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():
|
def test_config_to_docker_multiplatform():
|
||||||
graphs = {
|
graphs = {
|
||||||
"python": "./multiplatform/python.py:graph",
|
"python": "./multiplatform/python.py:graph",
|
||||||
|
|||||||
Reference in New Issue
Block a user