From 4768bdb0c89911e2d679c9b33bc56b68f4d12bdf Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 27 Jun 2024 09:10:21 -0700 Subject: [PATCH] cli: Add back up (#854) * Revert "cli: Reduce to test and build commands (#838)" This reverts commit 5697f0716301dd150f3ecb906c245619ee759342. * Undo * Undo * Undo * Fix * Fix * Add license key admonition * Quote env vars * Test --- libs/cli/langgraph_cli/cli.py | 200 +++++++++++++++++++++ libs/cli/langgraph_cli/config.py | 17 +- libs/cli/langgraph_cli/docker.py | 117 +++++++++++- libs/cli/tests/unit_tests/test_cli.py | 103 +++++++++++ libs/cli/tests/unit_tests/test_config.json | 8 +- libs/cli/tests/unit_tests/test_config.py | 177 +++++++++++++++++- libs/cli/tests/unit_tests/test_docker.py | 101 +++++++++++ 7 files changed, 703 insertions(+), 20 deletions(-) create mode 100644 libs/cli/tests/unit_tests/test_cli.py create mode 100644 libs/cli/tests/unit_tests/test_docker.py diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index 46d62bc15..e022c9e6c 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -9,10 +9,24 @@ import click.exceptions import langgraph_cli.config import langgraph_cli.docker from langgraph_cli.analytics import log_command +from langgraph_cli.config import Config from langgraph_cli.constants import DEFAULT_CONFIG, DEFAULT_PORT +from langgraph_cli.docker import DockerCapabilities from langgraph_cli.exec import Runner, subp_exec from langgraph_cli.progress import Progress +OPT_DOCKER_COMPOSE = click.option( + "--docker-compose", + "-d", + help="Advanced: Path to docker-compose.yml file with additional services to launch.", + type=click.Path( + exists=True, + file_okay=True, + dir_okay=False, + resolve_path=True, + path_type=pathlib.Path, + ), +) OPT_CONFIG = click.option( "--config", "-c", @@ -89,6 +103,12 @@ OPT_PORT = click.option( \b """, ) +OPT_RECREATE = click.option( + "--recreate/--no-recreate", + default=False, + show_default=True, + help="Recreate containers even if their configuration and image haven't changed", +) OPT_PULL = click.option( "--pull/--no-pull", default=True, @@ -108,6 +128,16 @@ OPT_VERBOSE = click.option( default=False, 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_POSTGRES_URI = click.option( + "--postgres-uri", + help="Postgres URI to use for the database. Defaults to launching a local database", +) @click.group() @@ -115,6 +145,101 @@ def cli(): pass +@OPT_RECREATE +@OPT_PULL +@OPT_PORT +@OPT_DOCKER_COMPOSE +@OPT_CONFIG +@OPT_VERBOSE +@OPT_DEBUGGER_PORT +@OPT_WATCH +@OPT_POSTGRES_URI +@click.option( + "--wait", + is_flag=True, + help="Wait for services to start before returning. Implies --detach", +) +@cli.command(help="Start langgraph API server. Requires a license key.") +@log_command +def up( + config: pathlib.Path, + docker_compose: Optional[pathlib.Path], + port: int, + recreate: bool, + pull: bool, + watch: bool, + wait: bool, + verbose: bool, + debugger_port: Optional[int], + postgres_uri: Optional[str], +): + with Runner() as runner, Progress(message="Pulling...") as set: + capabilities = langgraph_cli.docker.check_capabilities(runner) + args, stdin = prepare( + runner, + capabilities=capabilities, + config_path=config, + docker_compose=docker_compose, + port=port, + pull=pull, + watch=watch, + verbose=verbose, + debugger_port=debugger_port, + postgres_uri=postgres_uri, + ) + # add up + options + args.extend(["up", "--remove-orphans"]) + if recreate: + args.extend(["--force-recreate", "--renew-anon-volumes"]) + try: + runner.run(subp_exec("docker", "volume", "rm", "langgraph-data")) + except click.exceptions.Exit: + pass + if watch: + args.append("--watch") + if wait: + args.append("--wait") + else: + args.append("--abort-on-container-exit") + # run docker compose + set("Building...") + + def on_stdout(line: str): + if "unpacking to docker.io" in line: + set("Starting...") + elif "GET /ok" in line: + debugger_origin = ( + f"http://localhost:{debugger_port}" + if debugger_port + else "https://smith.langchain.com" + ) + set("") + sys.stdout.write( + f"""Ready! +- API: http://localhost:{port} +- Docs: http://localhost:{port}/docs +- Debugger: {debugger_origin}/studio/?baseUrl=http://127.0.0.1:{port} +""" + ) + sys.stdout.flush() + return True + + if capabilities.compose_type == "plugin": + compose_cmd = ["docker", "compose"] + elif capabilities.compose_type == "standalone": + compose_cmd = ["docker-compose"] + + runner.run( + subp_exec( + *compose_cmd, + *args, + input=stdin, + verbose=verbose, + on_stdout=on_stdout, + ) + ) + + @OPT_PULL @OPT_PORT @OPT_CONFIG @@ -304,3 +429,78 @@ def build( config_json = langgraph_cli.config.validate_config(json.load(f)) # build _build(runner, set, config, config_json, platform, base_image, pull, tag) + + +def prepare_args_and_stdin( + *, + capabilities: DockerCapabilities, + config_path: pathlib.Path, + config: Config, + docker_compose: Optional[pathlib.Path], + port: int, + watch: bool, + debugger_port: Optional[int] = None, + postgres_uri: Optional[str] = None, +): + # prepare args + stdin = langgraph_cli.docker.compose( + capabilities, + port=port, + debugger_port=debugger_port, + postgres_uri=postgres_uri, + ) + args = [ + "--project-directory", + str(config_path.parent), + ] + # apply options + if docker_compose: + args.extend(["-f", str(docker_compose)]) + args.extend(["-f", "-"]) # stdin + # apply config + stdin += langgraph_cli.config.config_to_compose( + config_path, + config, + watch=watch, + base_image="langchain/langgraph-api", + ) + return args, stdin + + +def prepare( + runner, + *, + capabilities: DockerCapabilities, + config_path: pathlib.Path, + docker_compose: Optional[pathlib.Path], + port: int, + pull: bool, + watch: bool, + verbose: bool, + debugger_port: Optional[int] = None, + postgres_uri: Optional[str] = None, +): + with open(config_path) as f: + config = langgraph_cli.config.validate_config(json.load(f)) + # pull latest images + if pull: + runner.run( + subp_exec( + "docker", + "pull", + f"langchain/langgraph-api:{config['python_version']}", + verbose=verbose, + ) + ) + + args, stdin = prepare_args_and_stdin( + capabilities=capabilities, + config_path=config_path, + config=config, + docker_compose=docker_compose, + port=port, + watch=watch, + debugger_port=debugger_port, + postgres_uri=postgres_uri, + ) + return args, stdin diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index cdbf6ee8e..605d7e3a6 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -265,30 +265,21 @@ def config_to_compose( config: Config, base_image: str, watch: bool = False, - langgraph_api_path: Optional[pathlib.Path] = None, ): env_vars = config["env"].items() if isinstance(config["env"], dict) else {} - env_vars_str = "\n".join(f" {k}: {v}" for k, v in env_vars) + env_vars_str = "\n".join(f' {k}: "{v}"' for k, v in env_vars) env_file_str = ( f"env_file: {config['env']}" if isinstance(config["env"], str) else "" ) if watch: - watch_paths = [config_path] + [ - config_path.parent / dep - for dep in config["dependencies"] - if dep.startswith(".") + watch_paths = [config_path.name] + [ + dep for dep in config["dependencies"] if dep.startswith(".") ] watch_actions = "\n".join( f"""- path: {path} - action: rebuild - ignore: - - .langgraph-data""" + action: rebuild""" for path in watch_paths ) - if langgraph_api_path: - watch_actions += f"""\n- path: {langgraph_api_path} - action: sync+restart - target: /api/langgraph_api""" watch_str = f""" develop: watch: diff --git a/libs/cli/langgraph_cli/docker.py b/libs/cli/langgraph_cli/docker.py index 050859423..1ea991a22 100644 --- a/libs/cli/langgraph_cli/docker.py +++ b/libs/cli/langgraph_cli/docker.py @@ -1,13 +1,47 @@ import json import pathlib import shutil -from typing import NamedTuple +from typing import Literal, NamedTuple, Optional import click.exceptions from langgraph_cli.exec import subp_exec ROOT = pathlib.Path(__file__).parent.resolve() +DEFAULT_POSTGRES_URI = ( + "postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable" +) + + +DB = """ + langgraph-postgres: + image: postgres:16 + ports: + - "5433:5432" + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + volumes: + - langgraph-data:/var/lib/postgresql/data + healthcheck: + test: pg_isready -U postgres + start_period: 10s + timeout: 1s + retries: 5 +""" + + +DEBUGGER = """ + langgraph-debugger: + image: langchain/langgraph-debugger + restart: on-failure + ports: + - "{debugger_port}:3968" + depends_on: + langgraph-postgres: + condition: service_healthy +""" class Version(NamedTuple): @@ -16,9 +50,14 @@ class Version(NamedTuple): patch: int +DockerComposeType = Literal["plugin", "standalone"] + + class DockerCapabilities(NamedTuple): version_docker: Version + version_compose: Version healthcheck_start_interval: bool + compose_type: DockerComposeType = "plugin" def _parse_version(version: str) -> Version: @@ -49,11 +88,87 @@ def check_capabilities(runner) -> DockerCapabilities: if not info["ServerVersion"]: raise click.UsageError("Docker not running") from None + compose_type: DockerComposeType + try: + compose = next( + p for p in info["ClientInfo"]["Plugins"] if p["Name"] == "compose" + ) + compose_version_str = compose["Version"] + compose_type = "plugin" + except (KeyError, StopIteration): + if shutil.which("docker-compose") is None: + raise click.UsageError("Docker Compose not installed") from None + + compose_version_str, _ = runner.run( + subp_exec("docker-compose", "--version", "--short", collect=True) + ) + compose_type = "standalone" + # parse versions docker_version = _parse_version(info["ServerVersion"]) + compose_version = _parse_version(compose_version_str) # check capabilities return DockerCapabilities( version_docker=docker_version, + version_compose=compose_version, healthcheck_start_interval=docker_version >= Version(25, 0, 0), + compose_type=compose_type, ) + + +def compose( + capabilities: DockerCapabilities, + *, + port: int, + debugger_port: Optional[int] = None, + # postgres://user:password@host:port/database?option=value + postgres_uri: Optional[str] = None, +) -> str: + if postgres_uri is None: + include_db = True + postgres_uri = DEFAULT_POSTGRES_URI + else: + include_db = False + + db = DB.format() if include_db else "" + volumes = ( + """volumes: + langgraph-data: + driver: local +""" + if include_db + else "" + ) + if db: + if capabilities.healthcheck_start_interval: + db += """ + interval: 60s + start_interval: 1s""" + else: + db += """ + interval: 5s""" + + compose_str = f"""{volumes}services: +{db} +{DEBUGGER.format(debugger_port=debugger_port) if debugger_port else ""} + langgraph-api: + ports: + - "{port}:8000\"""" + if include_db: + compose_str += """ + depends_on: + langgraph-postgres: + condition: service_healthy""" + compose_str += f""" + environment: + POSTGRES_URI: {postgres_uri} +""" + if capabilities.healthcheck_start_interval: + compose_str += """ healthcheck: + test: python /api/healthcheck.py + interval: 60s + start_interval: 1s + start_period: 10s""" + + return compose_str diff --git a/libs/cli/tests/unit_tests/test_cli.py b/libs/cli/tests/unit_tests/test_cli.py new file mode 100644 index 000000000..3e54223be --- /dev/null +++ b/libs/cli/tests/unit_tests/test_cli.py @@ -0,0 +1,103 @@ +import pathlib + +from langgraph_cli.cli import prepare_args_and_stdin +from langgraph_cli.config import Config, validate_config +from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version +from langgraph_cli.util import clean_empty_lines + +DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities( + version_docker=Version(26, 1, 1), + version_compose=Version(2, 27, 0), + healthcheck_start_interval=True, +) + + +def test_prepare_args_and_stdin(): + # this basically serves as an end-to-end test for using config and docker helpers + config_path = pathlib.Path("./langgraph.json") + config = validate_config( + Config(dependencies=["."], graphs={"agent": "agent.py:graph"}) + ) + port = 8000 + debugger_port = 8001 + + actual_args, actual_stdin = prepare_args_and_stdin( + capabilities=DEFAULT_DOCKER_CAPABILITIES, + config_path=config_path, + config=config, + docker_compose="custom-docker-compose.yml", + port=port, + debugger_port=debugger_port, + watch=True, + ) + + expected_args = [ + "--project-directory", + ".", + "-f", + "custom-docker-compose.yml", + "-f", + "-", + ] + expected_stdin = f"""volumes: + langgraph-data: + driver: local +services: + langgraph-postgres: + image: postgres:16 + ports: + - "5433:5432" + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + volumes: + - langgraph-data:/var/lib/postgresql/data + healthcheck: + test: pg_isready -U postgres + start_period: 10s + timeout: 1s + retries: 5 + interval: 60s + start_interval: 1s + langgraph-debugger: + image: langchain/langgraph-debugger + restart: on-failure + ports: + - "{debugger_port}:3968" + depends_on: + langgraph-postgres: + condition: service_healthy + langgraph-api: + ports: + - "8000:8000" + depends_on: + langgraph-postgres: + condition: service_healthy + environment: + POSTGRES_URI: {DEFAULT_POSTGRES_URI} + healthcheck: + test: python /api/healthcheck.py + interval: 60s + start_interval: 1s + start_period: 10s + + pull_policy: build + build: + context: . + dockerfile_inline: | + FROM langchain/langgraph-api:3.11 + ADD . /deps/ + RUN pip install -c /api/constraints.txt -e /deps/* + ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}' + WORKDIR /deps/ + + develop: + watch: + - path: langgraph.json + action: rebuild + - path: . + action: rebuild\ +""" + assert actual_args == expected_args + assert clean_empty_lines(actual_stdin) == expected_stdin diff --git a/libs/cli/tests/unit_tests/test_config.json b/libs/cli/tests/unit_tests/test_config.json index 617c4d467..b75d747c4 100644 --- a/libs/cli/tests/unit_tests/test_config.json +++ b/libs/cli/tests/unit_tests/test_config.json @@ -1,15 +1,13 @@ { "python_version": "3.12", "pip_config_file": "pipconfig.txt", - "dockerfile_lines": [ - "ARG meow" - ], + "dockerfile_lines": ["ARG meow"], "dependencies": [ "langchain_openai", "." ], "graphs": { - "agent": "./agent.py:graph" + "agent": "tests/unit_tests/agent.py:graph" }, "env": ".env" -} +} \ No newline at end of file diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index ed16750cd..62f1dcd6d 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -4,7 +4,7 @@ import pathlib import click import pytest -from langgraph_cli.config import config_to_docker, validate_config +from langgraph_cli.config import config_to_compose, config_to_docker, validate_config from langgraph_cli.util import clean_empty_lines PATH_TO_CONFIG = pathlib.Path(__file__).parent / "test_config.json" @@ -230,3 +230,178 @@ RUN set -ex && \\ RUN PIP_CONFIG_FILE=/pipconfig.txt pip install -c /api/constraints.txt -e /deps/* ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'""" assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin + + +# config_to_compose +def test_config_to_compose_simple_config(): + graphs = {"agent": "./agent.py:graph"} + expected_compose_stdin = """\ + + pull_policy: build + build: + context: . + dockerfile_inline: | + FROM langchain/langgraph-api:3.11 + ADD . /deps/__outer_unit_tests/unit_tests + RUN set -ex && \\ + for line in '[project]' \\ + 'name = "unit_tests"' \\ + 'version = "0.1"' \\ + '[tool.setuptools.package-data]' \\ + '"*" = ["**/*"]'; do \\ + echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ + done + RUN pip install -c /api/constraints.txt -e /deps/* + ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' + WORKDIR /deps/__outer_unit_tests/unit_tests + """ + actual_compose_stdin = config_to_compose( + PATH_TO_CONFIG, + validate_config({"dependencies": ["."], "graphs": graphs}), + "langchain/langgraph-api", + ) + assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin + + +def test_config_to_compose_env_vars(): + graphs = {"agent": "./agent.py:graph"} + expected_compose_stdin = """ OPENAI_API_KEY: "key" + + pull_policy: build + build: + context: . + dockerfile_inline: | + FROM langchain/langgraph-api-custom:3.11 + ADD . /deps/__outer_unit_tests/unit_tests + RUN set -ex && \\ + for line in '[project]' \\ + 'name = "unit_tests"' \\ + 'version = "0.1"' \\ + '[tool.setuptools.package-data]' \\ + '"*" = ["**/*"]'; do \\ + echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ + done + RUN pip install -c /api/constraints.txt -e /deps/* + ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' + WORKDIR /deps/__outer_unit_tests/unit_tests + """ + openai_api_key = "key" + actual_compose_stdin = config_to_compose( + PATH_TO_CONFIG, + validate_config( + { + "dependencies": ["."], + "graphs": graphs, + "env": {"OPENAI_API_KEY": openai_api_key}, + } + ), + "langchain/langgraph-api-custom", + ) + assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin + + +def test_config_to_compose_env_file(): + graphs = {"agent": "./agent.py:graph"} + expected_compose_stdin = """\ + env_file: .env + pull_policy: build + build: + context: . + dockerfile_inline: | + FROM langchain/langgraph-api:3.11 + ADD . /deps/__outer_unit_tests/unit_tests + RUN set -ex && \\ + for line in '[project]' \\ + 'name = "unit_tests"' \\ + 'version = "0.1"' \\ + '[tool.setuptools.package-data]' \\ + '"*" = ["**/*"]'; do \\ + echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ + done + RUN pip install -c /api/constraints.txt -e /deps/* + ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' + WORKDIR /deps/__outer_unit_tests/unit_tests + """ + actual_compose_stdin = config_to_compose( + PATH_TO_CONFIG, + validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}), + "langchain/langgraph-api", + ) + assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin + + +def test_config_to_compose_watch(): + graphs = {"agent": "./agent.py:graph"} + expected_compose_stdin = """\ + + pull_policy: build + build: + context: . + dockerfile_inline: | + FROM langchain/langgraph-api:3.11 + ADD . /deps/__outer_unit_tests/unit_tests + RUN set -ex && \\ + for line in '[project]' \\ + 'name = "unit_tests"' \\ + 'version = "0.1"' \\ + '[tool.setuptools.package-data]' \\ + '"*" = ["**/*"]'; do \\ + echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ + done + RUN pip install -c /api/constraints.txt -e /deps/* + ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' + WORKDIR /deps/__outer_unit_tests/unit_tests + + develop: + watch: + - path: test_config.json + action: rebuild + - path: . + action: rebuild\ +""" + actual_compose_stdin = config_to_compose( + PATH_TO_CONFIG, + validate_config({"dependencies": ["."], "graphs": graphs}), + "langchain/langgraph-api", + watch=True, + ) + assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin + + +def test_config_to_compose_end_to_end(): + # test all of the above + langgraph API path + graphs = {"agent": "./agent.py:graph"} + expected_compose_stdin = """\ + env_file: .env + pull_policy: build + build: + context: . + dockerfile_inline: | + FROM langchain/langgraph-api:3.11 + ADD . /deps/__outer_unit_tests/unit_tests + RUN set -ex && \\ + for line in '[project]' \\ + 'name = "unit_tests"' \\ + 'version = "0.1"' \\ + '[tool.setuptools.package-data]' \\ + '"*" = ["**/*"]'; do \\ + echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ + done + RUN pip install -c /api/constraints.txt -e /deps/* + ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' + WORKDIR /deps/__outer_unit_tests/unit_tests + + develop: + watch: + - path: test_config.json + action: rebuild + - path: . + action: rebuild\ +""" + actual_compose_stdin = config_to_compose( + PATH_TO_CONFIG, + validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}), + "langchain/langgraph-api", + watch=True, + ) + assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin diff --git a/libs/cli/tests/unit_tests/test_docker.py b/libs/cli/tests/unit_tests/test_docker.py new file mode 100644 index 000000000..4d5e09b94 --- /dev/null +++ b/libs/cli/tests/unit_tests/test_docker.py @@ -0,0 +1,101 @@ +from langgraph_cli.docker import ( + DEFAULT_POSTGRES_URI, + DockerCapabilities, + Version, + compose, +) +from langgraph_cli.util import clean_empty_lines + +DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities( + version_docker=Version(26, 1, 1), + version_compose=Version(2, 27, 0), + healthcheck_start_interval=False, +) + + +def test_compose_with_no_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-api: + ports: + - "{port}:8000" + environment: + POSTGRES_URI: {custom_postgres_uri}""" + assert clean_empty_lines(actual_compose_str) == expected_compose_str + + +def test_compose_with_no_debugger_and_custom_db_with_healthcheck(): + port = 8123 + custom_postgres_uri = "custom_postgres_uri" + actual_compose_str = compose( + DEFAULT_DOCKER_CAPABILITIES._replace(healthcheck_start_interval=True), + port=port, + postgres_uri=custom_postgres_uri, + ) + expected_compose_str = f"""services: + langgraph-api: + ports: + - "{port}:8000" + environment: + POSTGRES_URI: {custom_postgres_uri} + healthcheck: + test: python /api/healthcheck.py + interval: 60s + start_interval: 1s + start_period: 10s""" + 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-api: + ports: + - "{port}:8000" + environment: + POSTGRES_URI: {custom_postgres_uri}""" + assert clean_empty_lines(actual_compose_str) == expected_compose_str + + +def test_compose_with_debugger_and_default_db(): + port = 8123 + actual_compose_str = compose(DEFAULT_DOCKER_CAPABILITIES, port=port) + expected_compose_str = f"""volumes: + langgraph-data: + driver: local +services: + langgraph-postgres: + image: postgres:16 + ports: + - "5433:5432" + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + volumes: + - langgraph-data:/var/lib/postgresql/data + healthcheck: + test: pg_isready -U postgres + start_period: 10s + timeout: 1s + retries: 5 + interval: 5s + langgraph-api: + ports: + - "{port}:8000" + depends_on: + langgraph-postgres: + condition: service_healthy + environment: + POSTGRES_URI: {DEFAULT_POSTGRES_URI}""" + assert clean_empty_lines(actual_compose_str) == expected_compose_str