Compare commits

..
Author SHA1 Message Date
John Kennedyandopen-swe[bot] <open-swe@users.noreply.github.com> 8e05912899 fix(cli): extend API healthcheck startup window
Keep one-second health probes active for slower graph imports so Compose does not stall for the steady-state interval.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-08-20 07:24:09 +00:00
John KennedyandGitHub f899af1c73 Merge branch 'main' into langster/remove-stale-debugger-pull 2026-08-20 00:07:35 -07:00
John Kennedy 5f479f1af5 fix(cli): preserve hosted Studio URL overrides 2026-08-11 09:39:33 -07:00
John Kennedy 33c3edfde2 fix(cli): drop unrelated uv export change 2026-08-11 09:39:24 -07:00
John Kennedyandopen-swe[bot] <open-swe@users.noreply.github.com> 292fd5787c fix(cli): include workspace metadata in uv exports
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-08-10 23:03:38 +00:00
John Kennedyandopen-swe[bot] <open-swe@users.noreply.github.com> b904db211c fix(cli): update integration runner arguments
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-08-10 22:53:15 +00:00
langsmith-fleet[bot] ecee22fa16 fix(cli): remove discontinued local debugger image 2026-08-06 00:01:58 +00:00
12 changed files with 245 additions and 663 deletions
+1 -3
View File
@@ -32,8 +32,6 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
docker_compose=None,
port=port,
watch=False,
debugger_port=None,
debugger_base_url=f"http://127.0.0.1:{port}",
postgres_uri=None,
api_version=None,
image=tag,
@@ -173,5 +171,5 @@ if __name__ == "__main__":
except BaseException:
logger.exception("Test failed")
raise
logger.info("Test execution finished")
+28 -33
View File
@@ -17,9 +17,6 @@ jobs:
python-version:
- "3.10"
- "3.14"
build_mode:
- layered
- flat
example:
- name: A
workdir: libs/cli/examples
@@ -33,11 +30,9 @@ jobs:
- name: D
workdir: libs/cli/examples/graphs_reqs_b
tag: langgraph-test-d
name: "CLI integration test (${{ matrix.python-version }}, ${{ matrix.example.name }}, ${{ matrix.build_mode }})"
name: "CLI integration test"
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
@@ -63,7 +58,7 @@ jobs:
if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch')
working-directory: ${{ matrix.example.workdir }}
run: |
langgraph build $BUILD_ARGS -t ${{ matrix.example.tag }}$TAG_SUFFIX
langgraph build -t ${{ matrix.example.tag }}
- 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 }}
@@ -76,98 +71,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 }}$TAG_SUFFIX
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
- 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 $BUILD_ARGS -t langgraph-test-e$TAG_SUFFIX
langgraph build -t langgraph-test-e
- 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 $BUILD_ARGS -t langgraph-test-f$TAG_SUFFIX -c apps/agent/langgraph.json --build-command "yarn run turbo build" --install-command "yarn install"
langgraph build -t langgraph-test-f -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 $BUILD_ARGS -t langgraph-test-g$TAG_SUFFIX -c apps/agent/langgraph.json
langgraph build -t langgraph-test-g -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$TAG_SUFFIX -c apps/agent/langgraph.json
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -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 $BUILD_ARGS -t langgraph-test-h$TAG_SUFFIX
langgraph build -t langgraph-test-h
- 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$TAG_SUFFIX
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
echo "Finished starting up langgraph-test-h"
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);")
LANGGRAPH_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h 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$TAG_SUFFIX 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 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$TAG_SUFFIX 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 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 $BUILD_ARGS -t langgraph-test-i$TAG_SUFFIX || [ $? -eq 1 ]
langgraph build -t langgraph-test-i || [ $? -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 $BUILD_ARGS -t langgraph-test-uv-simple$TAG_SUFFIX
langgraph build -t langgraph-test-uv-simple
- 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$TAG_SUFFIX
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-uv-simple
- 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 $BUILD_ARGS -t langgraph-test-uv-monorepo$TAG_SUFFIX
langgraph build -t langgraph-test-uv-monorepo
- 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$TAG_SUFFIX
timeout 60 python ../../../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-uv-monorepo
-1
View File
@@ -76,7 +76,6 @@ __pypackages__/
# Environments
.env
.env.*
.envrc
*.crt
*.key
-8
View File
@@ -2,14 +2,6 @@
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
-8
View File
@@ -2,14 +2,6 @@
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
-3
View File
@@ -48,9 +48,6 @@ def get_anonymized_params(
if kwargs.get("docker_compose"):
params["docker_compose"] = True
if kwargs.get("debugger_port"):
params["debugger_port"] = True
if kwargs.get("postgres_uri"):
params["postgres_uri"] = True
+89 -42
View File
@@ -5,6 +5,7 @@ import pathlib
import shutil
import sys
from collections.abc import Sequence
from urllib.parse import SplitResult, urlencode, urlsplit, urlunsplit
import click
import click.exceptions
@@ -140,17 +141,6 @@ OPT_VERBOSE = click.option(
help="Show more output from the server logs",
)
OPT_WATCH = click.option("--watch", is_flag=True, help="Restart on file changes")
OPT_DEBUGGER_PORT = click.option(
"--debugger-port",
type=int,
help="Pull the debugger image locally and serve the UI on specified port",
)
OPT_DEBUGGER_BASE_URL = click.option(
"--debugger-base-url",
type=str,
help="URL used by the debugger to access LangGraph API. Defaults to http://127.0.0.1:[PORT]",
)
OPT_POSTGRES_URI = click.option(
"--postgres-uri",
help="Postgres URI to use for the database. Defaults to launching a local database",
@@ -242,18 +232,94 @@ cli.add_command(deploy)
# ---------------------------------------------------------------------------
def _validated_http_url(value: str, option_name: str) -> SplitResult:
try:
parsed = urlsplit(value)
hostname = parsed.hostname
_ = parsed.port
except ValueError as exc:
raise click.UsageError(
f"{option_name} must be a valid HTTP(S) URL without credentials."
) from exc
if (
value != value.strip()
or parsed.scheme not in {"http", "https"}
or not parsed.netloc
or not hostname
or parsed.username is not None
or parsed.password is not None
):
raise click.UsageError(
f"{option_name} must be a valid HTTP(S) URL without credentials."
)
return parsed
def _studio_link(
*,
port: int,
studio_url: str | None,
api_url: str | None,
debugger_base_url: str | None,
) -> str:
if debugger_base_url is not None:
if api_url is not None and api_url != debugger_base_url:
raise click.UsageError(
"--api-url and --debugger-base-url cannot specify different URLs."
)
click.echo(
"Warning: --debugger-base-url is deprecated; use --api-url instead.",
err=True,
)
api_url = debugger_base_url
studio_url = "https://smith.langchain.com" if studio_url is None else studio_url
api_url = f"http://127.0.0.1:{port}" if api_url is None else api_url
studio_parts = _validated_http_url(studio_url, "--studio-url")
_validated_http_url(api_url, "--api-url")
if studio_parts.query or studio_parts.fragment:
raise click.UsageError(
"--studio-url must not include a query string or fragment."
)
studio_path = f"{studio_parts.path.rstrip('/')}/studio/"
return urlunsplit(
studio_parts._replace(
path=studio_path,
query=urlencode({"baseUrl": api_url}),
)
)
@OPT_RECREATE
@OPT_PULL
@OPT_PORT
@OPT_DOCKER_COMPOSE
@OPT_CONFIG
@OPT_VERBOSE
@OPT_DEBUGGER_PORT
@OPT_DEBUGGER_BASE_URL
@OPT_WATCH
@OPT_POSTGRES_URI
@OPT_API_VERSION
@OPT_ENGINE_RUNTIME_MODE
@click.option(
"--studio-url",
type=str,
default=None,
help="URL of the LangGraph Studio instance. Defaults to https://smith.langchain.com",
)
@click.option(
"--api-url",
type=str,
default=None,
help="URL that LangGraph Studio uses to access the API. Defaults to http://127.0.0.1:[PORT]",
)
@click.option(
"--debugger-base-url",
type=str,
default=None,
hidden=True,
)
@click.option(
"--image",
type=str,
@@ -284,14 +350,21 @@ def up(
watch: bool,
wait: bool,
verbose: bool,
debugger_port: int | None,
debugger_base_url: str | None,
postgres_uri: str | None,
api_version: str | None,
engine_runtime_mode: str,
studio_url: str | None,
api_url: str | None,
debugger_base_url: str | None,
image: str | None,
base_image: str | None,
):
studio_link = _studio_link(
port=port,
studio_url=studio_url,
api_url=api_url,
debugger_base_url=debugger_base_url,
)
click.secho("Starting LangGraph API server...", fg="green")
click.secho(
"""For local dev, requires env var LANGSMITH_API_KEY with access to LangSmith Deployment.
@@ -308,8 +381,6 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
pull=pull,
watch=watch,
verbose=verbose,
debugger_port=debugger_port,
debugger_base_url=debugger_base_url,
postgres_uri=postgres_uri,
api_version=api_version,
engine_runtime_mode=engine_runtime_mode,
@@ -337,20 +408,12 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
if "unpacking to docker.io" in line:
set("Starting...")
elif "Application startup complete" in line:
debugger_origin = (
f"http://localhost:{debugger_port}"
if debugger_port
else "https://smith.langchain.com"
)
debugger_base_url_query = (
debugger_base_url or f"http://127.0.0.1:{port}"
)
set("")
sys.stdout.write(
f"""Ready!
- API: http://localhost:{port}
- Docs: http://localhost:{port}/docs
- LangGraph Studio: {debugger_origin}/studio/?baseUrl={debugger_base_url_query}
- LangGraph Studio: {studio_link}
"""
)
sys.stdout.flush()
@@ -408,12 +471,6 @@ 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.",
@@ -432,7 +489,6 @@ 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
@@ -468,7 +524,6 @@ def build(
docker_build_args,
install_command,
build_command,
flat=flat,
)
@@ -943,8 +998,6 @@ def prepare_args_and_stdin(
docker_compose: pathlib.Path | None,
port: int,
watch: bool,
debugger_port: int | None = None,
debugger_base_url: str | None = None,
postgres_uri: str | None = None,
api_version: str | None = None,
engine_runtime_mode: str = "combined_queue_worker",
@@ -958,8 +1011,6 @@ def prepare_args_and_stdin(
stdin = langgraph_cli.docker.compose(
capabilities,
port=port,
debugger_port=debugger_port,
debugger_base_url=debugger_base_url,
postgres_uri=postgres_uri,
image=image,
base_image=base_image,
@@ -997,8 +1048,6 @@ def prepare(
pull: bool,
watch: bool,
verbose: bool,
debugger_port: int | None = None,
debugger_base_url: str | None = None,
postgres_uri: str | None = None,
api_version: str | None = None,
engine_runtime_mode: str = "combined_queue_worker",
@@ -1040,8 +1089,6 @@ def prepare(
docker_compose=docker_compose,
port=port,
watch=watch,
debugger_port=debugger_port,
debugger_base_url=debugger_base_url or f"http://127.0.0.1:{port}",
postgres_uri=postgres_uri,
api_version=api_version,
engine_runtime_mode=engine_runtime_mode,
+42 -292
View File
@@ -1162,208 +1162,6 @@ 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
@@ -1469,7 +1267,6 @@ 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)
@@ -1631,36 +1428,6 @@ 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 = []
@@ -1675,6 +1442,14 @@ 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}",
@@ -1684,14 +1459,18 @@ ADD {relpath} /deps/{name}
installs,
"",
"# -- Installing all local dependencies --",
f"RUN {local_deps_install_command}",
local_deps_install_str,
"# -- End of local dependencies install --",
os.linesep.join(env_vars),
"",
js_inst_str,
"",
# Add pip cleanup after all installations are complete
pip_cleanup,
_get_pip_cleanup_lines(
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 "",
]
@@ -1700,31 +1479,6 @@ 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,
@@ -1733,7 +1487,6 @@ 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 = (
@@ -1743,56 +1496,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
install_workdir = f"/deps/{container_name}"
build_workdir = (
f"{install_workdir}/{relative_workdir}"
if relative_workdir
else install_workdir
)
if relative_workdir:
faux_path = f"/deps/{container_name}/{relative_workdir}"
else:
faux_path = f"/deps/{container_name}"
else:
# Backward compatibility: use the original behavior
install_workdir = build_workdir = f"/deps/{config_path.parent.name}"
faux_path = f"/deps/{config_path.parent.name}"
image_str = docker_tag(config, base_image, api_version)
env_vars = _build_runtime_env_vars(config)
build_cmd = (
build_command
if build_context and build_command
else _DEFAULT_NODE_BUILD_COMMAND
)
# 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}"
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,
),
{},
)
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
docker_file_contents = [
f"FROM {image_str}",
"",
os.linesep.join(config["dockerfile_lines"]),
"",
f"ADD . {install_workdir}",
f"ADD . {faux_path if not build_context else container_root}",
"",
f"WORKDIR {install_workdir}",
"",
f"RUN {install_cmd}",
install_step,
"",
os.linesep.join(env_vars),
"",
f"WORKDIR {build_workdir}",
"",
f"RUN {build_cmd}",
build_step,
]
return os.linesep.join(docker_file_contents), {}
@@ -1872,7 +1625,6 @@ 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)
@@ -1885,7 +1637,6 @@ def config_to_docker(
install_command=install_command,
build_command=build_command,
build_context=build_context,
flat=flat,
)
return python_config_to_docker(
@@ -1894,7 +1645,6 @@ def config_to_docker(
base_image=base_image,
api_version=api_version,
escape_variables=escape_variables,
flat=flat,
)
+1 -38
View File
@@ -142,29 +142,6 @@ def check_capabilities(runner) -> DockerCapabilities:
)
def debugger_compose(*, port: int | None = None, base_url: str | None = None) -> dict:
if port is None:
return ""
config = {
"langgraph-debugger": {
"image": "langchain/langgraph-debugger",
"restart": "on-failure",
"depends_on": {
"langgraph-postgres": {"condition": "service_healthy"},
},
"ports": [f'"{port}:3968"'],
}
}
if base_url:
config["langgraph-debugger"]["environment"] = {
"VITE_STUDIO_LOCAL_GRAPH_URL": base_url
}
return config
# Function to convert dictionary to YAML
def dict_to_yaml(d: dict, *, indent: int = 0) -> str:
"""Convert a dictionary to a YAML string."""
@@ -191,8 +168,6 @@ def compose_as_dict(
capabilities: DockerCapabilities,
*,
port: int,
debugger_port: int | None = None,
debugger_base_url: str | None = None,
# postgres://user:password@host:port/database?option=value
postgres_uri: str | None = None,
# If you are running against an already-built image, you can pass it here
@@ -253,12 +228,6 @@ def compose_as_dict(
else:
services["langgraph-postgres"]["healthcheck"]["interval"] = "5s"
# Add optional debugger service if debugger_port is specified
if debugger_port:
services["langgraph-debugger"] = debugger_compose(
port=debugger_port, base_url=debugger_base_url
)["langgraph-debugger"]
# Add langgraph-api service
api_environment = {
"REDIS_URI": "redis://langgraph-redis:6379",
@@ -289,7 +258,7 @@ def compose_as_dict(
"test": "python /api/healthcheck.py",
"interval": "60s",
"start_interval": "1s",
"start_period": "10s",
"start_period": "60s",
}
# Final compose dictionary with volumes included if needed
@@ -305,8 +274,6 @@ def compose(
capabilities: DockerCapabilities,
*,
port: int,
debugger_port: int | None = None,
debugger_base_url: str | None = None,
# postgres://user:password@host:port/database?option=value
postgres_uri: str | None = None,
image: str | None = None,
@@ -318,8 +285,6 @@ def compose(
compose_content = compose_as_dict(
capabilities,
port=port,
debugger_port=debugger_port,
debugger_base_url=debugger_base_url,
postgres_uri=postgres_uri,
image=image,
base_image=base_image,
@@ -345,7 +310,6 @@ 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
@@ -388,7 +352,6 @@ def build_docker_image(
install_command=install_command,
build_command=build_command,
build_context=build_context,
flat=flat,
)
# add additional_contexts
if additional_contexts:
+80 -31
View File
@@ -8,10 +8,11 @@ from contextlib import contextmanager
from pathlib import Path
import click
import pytest
from click.testing import CliRunner
import langgraph_cli.deploy as deploy_module
from langgraph_cli.cli import cli, prepare_args_and_stdin
from langgraph_cli.cli import _studio_link, cli, prepare_args_and_stdin
from langgraph_cli.config import Config, _get_pip_cleanup_lines, validate_config
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
from langgraph_cli.util import clean_empty_lines
@@ -56,8 +57,6 @@ def test_prepare_args_and_stdin() -> None:
Config(dependencies=[".", "../../.."], graphs={"agent": "agent.py:graph"})
)
port = 8000
debugger_port = 8001
debugger_graph_url = f"http://127.0.0.1:{port}"
actual_args, actual_stdin = prepare_args_and_stdin(
capabilities=DEFAULT_DOCKER_CAPABILITIES,
@@ -65,8 +64,6 @@ def test_prepare_args_and_stdin() -> None:
config=config,
docker_compose=pathlib.Path("custom-docker-compose.yml"),
port=port,
debugger_port=debugger_port,
debugger_base_url=debugger_graph_url,
watch=True,
)
@@ -110,16 +107,6 @@ services:
retries: 5
interval: 60s
start_interval: 1s
langgraph-debugger:
image: langchain/langgraph-debugger
restart: on-failure
depends_on:
langgraph-postgres:
condition: service_healthy
ports:
- "{debugger_port}:3968"
environment:
VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
langgraph-api:
ports:
- "8000:8000"
@@ -135,7 +122,7 @@ services:
test: python /api/healthcheck.py
interval: 60s
start_interval: 1s
start_period: 10s
start_period: 60s
pull_policy: build
build:
@@ -178,8 +165,6 @@ def test_prepare_args_and_stdin_with_image() -> None:
Config(dependencies=[".", "../../.."], graphs={"agent": "agent.py:graph"})
)
port = 8000
debugger_port = 8001
debugger_graph_url = f"http://127.0.0.1:{port}"
actual_args, actual_stdin = prepare_args_and_stdin(
capabilities=DEFAULT_DOCKER_CAPABILITIES,
@@ -187,8 +172,6 @@ def test_prepare_args_and_stdin_with_image() -> None:
config=config,
docker_compose=pathlib.Path("custom-docker-compose.yml"),
port=port,
debugger_port=debugger_port,
debugger_base_url=debugger_graph_url,
watch=True,
image="my-cool-image",
)
@@ -233,16 +216,6 @@ services:
retries: 5
interval: 60s
start_interval: 1s
langgraph-debugger:
image: langchain/langgraph-debugger
restart: on-failure
depends_on:
langgraph-postgres:
condition: service_healthy
ports:
- "{debugger_port}:3968"
environment:
VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
langgraph-api:
ports:
- "8000:8000"
@@ -259,7 +232,7 @@ services:
test: python /api/healthcheck.py
interval: 60s
start_interval: 1s
start_period: 10s
start_period: 60s
develop:
@@ -289,6 +262,82 @@ def test_version_option() -> None:
)
def test_up_help_shows_hosted_studio_options() -> None:
result = CliRunner().invoke(cli, ["up", "--help"])
assert result.exit_code == 0, result.output
assert "--studio-url" in result.output
assert "--api-url" in result.output
assert "--debugger-port" not in result.output
assert "--debugger-base-url" not in result.output
def test_studio_link_defaults_to_hosted_studio() -> None:
assert _studio_link(
port=8123,
studio_url=None,
api_url=None,
debugger_base_url=None,
) == ("https://smith.langchain.com/studio/?baseUrl=http%3A%2F%2F127.0.0.1%3A8123")
def test_studio_link_supports_self_hosted_and_remote_urls() -> None:
assert _studio_link(
port=8123,
studio_url="https://langsmith.example.com/prefix/",
api_url="https://api.example.com/graph?tenant=a&region=eu",
debugger_base_url=None,
) == (
"https://langsmith.example.com/prefix/studio/"
"?baseUrl=https%3A%2F%2Fapi.example.com%2Fgraph%3Ftenant%3Da%26region%3Deu"
)
def test_studio_link_supports_deprecated_debugger_base_url(capsys) -> None:
assert _studio_link(
port=8123,
studio_url=None,
api_url=None,
debugger_base_url="https://api.example.com",
).endswith("?baseUrl=https%3A%2F%2Fapi.example.com")
assert "--debugger-base-url is deprecated; use --api-url" in capsys.readouterr().err
@pytest.mark.parametrize(
("studio_url", "api_url"),
[
("javascript:alert(1)", None),
("https://user:password@example.com", None),
("https://smith.langchain.com?workspace=test", None),
(None, "file:///tmp/langgraph.sock"),
(None, "https://user:password@example.com"),
],
)
def test_studio_link_rejects_unsafe_urls(
studio_url: str | None, api_url: str | None
) -> None:
with pytest.raises(click.UsageError):
_studio_link(
port=8123,
studio_url=studio_url,
api_url=api_url,
debugger_base_url=None,
)
def test_studio_link_rejects_conflicting_api_url_aliases() -> None:
with pytest.raises(
click.UsageError,
match="cannot specify different URLs",
):
_studio_link(
port=8123,
studio_url=None,
api_url="https://api.example.com",
debugger_base_url="https://other.example.com",
)
def test_top_level_help_shows_deploy_subcommands() -> None:
runner = CliRunner()
-106
View File
@@ -723,67 +723,6 @@ 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(
@@ -1059,28 +998,6 @@ 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"}
@@ -1297,29 +1214,6 @@ 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",
+4 -98
View File
@@ -16,7 +16,7 @@ DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
)
def test_compose_with_no_debugger_and_custom_db():
def test_compose_with_custom_db():
port = 8123
custom_postgres_uri = "custom_postgres_uri"
actual_compose_str = compose(
@@ -42,7 +42,7 @@ def test_compose_with_no_debugger_and_custom_db():
assert clean_empty_lines(actual_compose_str) == expected_compose_str
def test_compose_with_no_debugger_and_custom_db_with_healthcheck():
def test_compose_with_custom_db_and_healthcheck():
port = 8123
custom_postgres_uri = "custom_postgres_uri"
actual_compose_str = compose(
@@ -71,39 +71,11 @@ def test_compose_with_no_debugger_and_custom_db_with_healthcheck():
test: python /api/healthcheck.py
interval: 60s
start_interval: 1s
start_period: 10s"""
start_period: 60s"""
assert clean_empty_lines(actual_compose_str) == expected_compose_str
def test_compose_with_debugger_and_custom_db():
port = 8123
custom_postgres_uri = "custom_postgres_uri"
actual_compose_str = compose(
DEFAULT_DOCKER_CAPABILITIES,
port=port,
postgres_uri=custom_postgres_uri,
)
expected_compose_str = f"""services:
langgraph-redis:
image: redis:6
healthcheck:
test: redis-cli ping
interval: 5s
timeout: 1s
retries: 5
langgraph-api:
ports:
- "{port}:8000"
depends_on:
langgraph-redis:
condition: service_healthy
environment:
REDIS_URI: redis://langgraph-redis:6379
POSTGRES_URI: {custom_postgres_uri}"""
assert clean_empty_lines(actual_compose_str) == expected_compose_str
def test_compose_with_debugger_and_default_db():
def test_compose_with_default_db():
port = 8123
actual_compose_str = compose(DEFAULT_DOCKER_CAPABILITIES, port=port)
expected_compose_str = f"""volumes:
@@ -302,72 +274,6 @@ def test_compose_with_api_version_and_custom_postgres():
assert clean_empty_lines(actual_compose_str) == expected_compose_str
def test_compose_with_api_version_and_debugger():
"""Test compose function with api_version and debugger port."""
port = 8123
debugger_port = 8001
api_version = "0.2.74"
actual_compose_str = compose(
DEFAULT_DOCKER_CAPABILITIES,
port=port,
api_version=api_version,
debugger_port=debugger_port,
)
expected_compose_str = f"""volumes:
langgraph-data:
driver: local
services:
langgraph-redis:
image: redis:6
healthcheck:
test: redis-cli ping
interval: 5s
timeout: 1s
retries: 5
langgraph-postgres:
image: pgvector/pgvector:pg16
ports:
- "5433:5432"
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
command:
- postgres
- -c
- shared_preload_libraries=vector
volumes:
- langgraph-data:/var/lib/postgresql/data
healthcheck:
test: pg_isready -U postgres
start_period: 10s
timeout: 1s
retries: 5
interval: 5s
langgraph-debugger:
image: langchain/langgraph-debugger
restart: on-failure
depends_on:
langgraph-postgres:
condition: service_healthy
ports:
- "{debugger_port}:3968"
langgraph-api:
ports:
- "{port}:8000"
depends_on:
langgraph-redis:
condition: service_healthy
langgraph-postgres:
condition: service_healthy
environment:
REDIS_URI: redis://langgraph-redis:6379
POSTGRES_URI: {DEFAULT_POSTGRES_URI}"""
assert clean_empty_lines(actual_compose_str) == expected_compose_str
def test_compose_distributed_mode_with_custom_db():
"""Test compose with engine_runtime_mode='distributed' adds N_JOBS_PER_WORKER=0."""
port = 8123