diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index 91980d3ae..8ca1594a6 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -153,6 +153,12 @@ OPT_POSTGRES_URI = click.option( help="Postgres URI to use for the database. Defaults to launching a local database", ) +OPT_API_VERSION = click.option( + "--api-version", + type=str, + help="API server version to use for the base image. If unspecified, the latest version will be used.", +) + @click.group() @click.version_option(version=__version__, prog_name="LangGraph CLI") @@ -170,6 +176,7 @@ def cli(): @OPT_DEBUGGER_BASE_URL @OPT_WATCH @OPT_POSTGRES_URI +@OPT_API_VERSION @click.option( "--image", type=str, @@ -203,6 +210,7 @@ def up( debugger_port: Optional[int], debugger_base_url: Optional[str], postgres_uri: Optional[str], + api_version: Optional[str], image: Optional[str], base_image: Optional[str], ): @@ -225,6 +233,7 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE debugger_port=debugger_port, debugger_base_url=debugger_base_url, postgres_uri=postgres_uri, + api_version=api_version, image=image, base_image=base_image, ) @@ -290,6 +299,7 @@ def _build( config: pathlib.Path, config_json: dict, base_image: Optional[str], + api_version: Optional[str], pull: bool, tag: str, passthrough: Sequence[str] = (), @@ -300,7 +310,7 @@ def _build( subp_exec( "docker", "pull", - langgraph_cli.config.docker_tag(config_json, base_image), + langgraph_cli.config.docker_tag(config_json, base_image, api_version), verbose=True, ) ) @@ -314,7 +324,7 @@ def _build( ] # apply config stdin, additional_contexts = langgraph_cli.config.config_to_docker( - config, config_json, base_image + config, config_json, base_image, api_version ) # add additional_contexts if additional_contexts: @@ -355,6 +365,7 @@ def _build( "\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version" "\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)", ) +@OPT_API_VERSION @click.argument("docker_build_args", nargs=-1, type=click.UNPROCESSED) @cli.command( help="📦 Build LangGraph API server Docker image.", @@ -367,6 +378,7 @@ def build( config: pathlib.Path, docker_build_args: Sequence[str], base_image: Optional[str], + api_version: Optional[str], pull: bool, tag: str, ): @@ -376,7 +388,7 @@ def build( config_json = langgraph_cli.config.validate_config_file(config) warn_non_wolfi_distro(config_json) _build( - runner, set, config, config_json, base_image, pull, tag, docker_build_args + runner, set, config, config_json, base_image, api_version, pull, tag, docker_build_args ) @@ -456,12 +468,14 @@ tests "\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version" "\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)", ) +@OPT_API_VERSION @log_command def dockerfile( save_path: str, config: pathlib.Path, add_docker_compose: bool, base_image: Optional[str] = None, + api_version: Optional[str] = None, ) -> None: save_path = pathlib.Path(save_path).absolute() secho(f"🔍 Validating configuration at path: {config}", fg="yellow") @@ -474,6 +488,7 @@ def dockerfile( config, config_json, base_image=base_image, + api_version=api_version, ) with open(str(save_path), "w", encoding="utf-8") as f: f.write(dockerfile) @@ -739,11 +754,12 @@ def prepare_args_and_stdin( debugger_port: Optional[int] = None, debugger_base_url: Optional[str] = None, postgres_uri: Optional[str] = None, + api_version: Optional[str] = None, # Like "my-tag" (if you already built it locally) image: Optional[str] = None, # Like "langchain/langgraphjs-api" or "langchain/langgraph-api base_image: Optional[str] = None, -) -> tuple[list[str], str]: +): assert config_path.exists(), f"Config file not found: {config_path}" # prepare args stdin = langgraph_cli.docker.compose( @@ -754,6 +770,7 @@ def prepare_args_and_stdin( postgres_uri=postgres_uri, image=image, # Pass image to compose YAML generator base_image=base_image, + api_version=api_version, ) args = [ "--project-directory", @@ -769,6 +786,7 @@ def prepare_args_and_stdin( config, watch=watch, base_image=langgraph_cli.config.default_base_image(config), + api_version=api_version, image=image, ) return args, stdin @@ -787,6 +805,7 @@ def prepare( debugger_port: Optional[int] = None, debugger_base_url: Optional[str] = None, postgres_uri: Optional[str] = None, + api_version: Optional[str] = None, image: Optional[str] = None, base_image: Optional[str] = None, ) -> tuple[list[str], str]: @@ -799,7 +818,7 @@ def prepare( subp_exec( "docker", "pull", - langgraph_cli.config.docker_tag(config_json, base_image), + langgraph_cli.config.docker_tag(config_json, base_image, api_version), verbose=verbose, ) ) @@ -814,6 +833,7 @@ def prepare( 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, image=image, base_image=base_image, ) diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index ece457aad..e6d2280e5 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -1133,6 +1133,7 @@ def python_config_to_docker( config_path: pathlib.Path, config: Config, base_image: str, + api_version: Optional[str] = None, ) -> tuple[str, dict[str, str]]: """Generate a Dockerfile from the configuration.""" pip_installer = config.get("pip_installer", "auto") @@ -1282,7 +1283,7 @@ ADD {relpath} /deps/{name} "# -- End of JS dependencies install --", ] ) - image_str = docker_tag(config, base_image) + image_str = docker_tag(config, base_image, api_version) docker_file_contents = [ f"FROM {image_str}", "", @@ -1320,10 +1321,11 @@ def node_config_to_docker( config_path: pathlib.Path, config: Config, base_image: str, + api_version: Optional[str] = None, ) -> tuple[str, dict[str, str]]: faux_path = f"/deps/{config_path.parent.name}" install_cmd = _get_node_pm_install_cmd(config_path, config) - image_str = docker_tag(config, base_image) + image_str = docker_tag(config, base_image, api_version) env_vars: list[str] = [] @@ -1379,6 +1381,7 @@ def default_base_image(config: Config) -> str: def docker_tag( config: Config, base_image: Optional[str] = None, + api_version: Optional[str] = None, ) -> str: base_image = base_image or default_base_image(config) @@ -1391,28 +1394,43 @@ def docker_tag( if "/langgraph-server" in base_image: return f"{base_image}-py{config['python_version']}" + # Build the standard tag format + language, version = None, None if config.get("node_version") and not config.get("python_version"): - return f"{base_image}:{config['node_version']}{distro_tag}" - return f"{base_image}:{config['python_version']}{distro_tag}" + language, version = "node", config["node_version"] + else: + language, version = "py", config["python_version"] + + lang_distro_tag = f"{version}{distro_tag}" + + # Prepend API version if provided + if api_version: + full_tag = f"{api_version}-{language}{lang_distro_tag}" + else: + full_tag = lang_distro_tag + + return f"{base_image}:{full_tag}" def config_to_docker( config_path: pathlib.Path, config: Config, base_image: Optional[str] = None, + api_version: Optional[str] = None, ) -> tuple[str, dict[str, str]]: base_image = base_image or default_base_image(config) if config.get("node_version") and not config.get("python_version"): - return node_config_to_docker(config_path, config, base_image) + return node_config_to_docker(config_path, config, base_image, api_version) - return python_config_to_docker(config_path, config, base_image) + return python_config_to_docker(config_path, config, base_image, api_version) def config_to_compose( config_path: pathlib.Path, config: Config, base_image: Optional[str] = None, + api_version: Optional[str] = None, image: Optional[str] = None, watch: bool = False, ) -> str: @@ -1449,7 +1467,7 @@ def config_to_compose( else: dockerfile, additional_contexts = config_to_docker( - config_path, config, base_image + config_path, config, base_image, api_version ) additional_contexts_str = "\n".join( diff --git a/libs/cli/langgraph_cli/docker.py b/libs/cli/langgraph_cli/docker.py index ff820b1da..614dc6369 100644 --- a/libs/cli/langgraph_cli/docker.py +++ b/libs/cli/langgraph_cli/docker.py @@ -147,6 +147,8 @@ def compose_as_dict( image: Optional[str] = None, # Base image to use for the LangGraph API server base_image: Optional[str] = None, + # API version of the base image + api_version: Optional[str] = None, ) -> dict: """Create a docker compose file as a dictionary in YML style.""" if postgres_uri is None: @@ -252,6 +254,7 @@ def compose( postgres_uri: Optional[str] = None, image: Optional[str] = None, base_image: Optional[str] = None, + api_version: Optional[str] = None, ) -> str: """Create a docker compose file as a string.""" compose_content = compose_as_dict( @@ -262,6 +265,7 @@ def compose( postgres_uri=postgres_uri, image=image, base_image=base_image, + api_version=api_version, ) compose_str = dict_to_yaml(compose_content) return compose_str diff --git a/libs/cli/tests/unit_tests/cli/test_cli.py b/libs/cli/tests/unit_tests/cli/test_cli.py index d07d6f7a8..854e99304 100644 --- a/libs/cli/tests/unit_tests/cli/test_cli.py +++ b/libs/cli/tests/unit_tests/cli/test_cli.py @@ -573,3 +573,238 @@ def test_build_generate_proper_build_context(): assert ( len(build_contexts) == 2 ), f"Expected 2 build contexts, but found {len(build_contexts)}" + + +def test_dockerfile_command_with_api_version() -> None: + """Test the 'dockerfile' command with --api-version flag.""" + runner = CliRunner() + config_content = { + "python_version": "3.11", + "graphs": {"agent": "agent.py:graph"}, + "dependencies": ["."], + } + + with temporary_config_folder(config_content) as temp_dir: + save_path = temp_dir / "Dockerfile" + agent_path = temp_dir / "agent.py" + agent_path.touch() + + result = runner.invoke( + cli, + [ + "dockerfile", + str(save_path), + "--config", str(temp_dir / "config.json"), + "--api-version", "0.2.74" + ], + ) + + # Assert command was successful + assert result.exit_code == 0, result.output + assert "✅ Created: Dockerfile" in result.output + + # Check if Dockerfile was created and contains correct FROM line + assert save_path.exists() + with open(save_path) as f: + dockerfile = f.read() + assert "FROM langchain/langgraph-api:0.2.74-py3.11" in dockerfile + + +def test_dockerfile_command_with_api_version_and_base_image() -> None: + """Test the 'dockerfile' command with both --api-version and --base-image flags.""" + runner = CliRunner() + config_content = { + "python_version": "3.12", + "graphs": {"agent": "agent.py:graph"}, + "dependencies": ["."], + "image_distro": "wolfi", + } + + with temporary_config_folder(config_content) as temp_dir: + save_path = temp_dir / "Dockerfile" + agent_path = temp_dir / "agent.py" + agent_path.touch() + + result = runner.invoke( + cli, + [ + "dockerfile", + str(save_path), + "--config", str(temp_dir / "config.json"), + "--api-version", "1.0.0", + "--base-image", "my-registry/custom-api" + ], + ) + + # Assert command was successful + assert result.exit_code == 0, result.output + assert "✅ Created: Dockerfile" in result.output + + # Check if Dockerfile was created and contains correct FROM line + assert save_path.exists() + with open(save_path) as f: + dockerfile = f.read() + assert "FROM my-registry/custom-api:1.0.0-py3.12-wolfi" in dockerfile + + +def test_dockerfile_command_with_api_version_nodejs() -> None: + """Test the 'dockerfile' command with --api-version flag for Node.js config.""" + runner = CliRunner() + config_content = { + "node_version": "20", + "graphs": {"agent": "agent.js:graph"}, + } + + with temporary_config_folder(config_content) as temp_dir: + save_path = temp_dir / "Dockerfile" + agent_path = temp_dir / "agent.js" + agent_path.touch() + + result = runner.invoke( + cli, + [ + "dockerfile", + str(save_path), + "--config", str(temp_dir / "config.json"), + "--api-version", "0.2.74" + ], + ) + + # Assert command was successful + assert result.exit_code == 0, result.output + assert "✅ Created: Dockerfile" in result.output + + # Check if Dockerfile was created and contains correct FROM line + assert save_path.exists() + with open(save_path) as f: + dockerfile = f.read() + assert "FROM langchain/langgraphjs-api:0.2.74-node20" in dockerfile + + +def test_build_command_with_api_version() -> None: + """Test the 'build' command with --api-version flag.""" + runner = CliRunner() + config_content = { + "python_version": "3.11", + "graphs": {"agent": "agent.py:graph"}, + "dependencies": ["."], + "image_distro": "wolfi", # Use wolfi to avoid warning messages + } + + with temporary_config_folder(config_content) as temp_dir: + agent_path = temp_dir / "agent.py" + agent_path.touch() + + # Mock docker command since we don't want to actually build + with runner.isolated_filesystem(): + result = runner.invoke( + cli, + [ + "build", + "--tag", + "test-image", + "--config", + str(temp_dir / "config.json"), + "--api-version", "0.2.74", + "--no-pull" # Avoid pulling non-existent images + ], + catch_exceptions=True, + ) + + # Check that the build command is called with the correct tag + # The output should contain the docker build command with the api_version tag + assert "langchain/langgraph-api:0.2.74-py3.11-wolfi" in result.output + + +def test_build_command_with_api_version_and_base_image() -> None: + """Test the 'build' command with both --api-version and --base-image flags.""" + runner = CliRunner() + config_content = { + "python_version": "3.12", + "graphs": {"agent": "agent.py:graph"}, + "dependencies": ["."], + "image_distro": "wolfi", # Use wolfi to avoid warning messages + } + + with temporary_config_folder(config_content) as temp_dir: + agent_path = temp_dir / "agent.py" + agent_path.touch() + + # Mock docker command since we don't want to actually build + with runner.isolated_filesystem(): + result = runner.invoke( + cli, + [ + "build", + "--tag", + "test-image", + "--config", + str(temp_dir / "config.json"), + "--api-version", "1.0.0", + "--base-image", "my-registry/custom-api", + "--no-pull" # Avoid pulling non-existent images + ], + catch_exceptions=True, + ) + + # Check that the build command includes the api_version + assert "my-registry/custom-api:1.0.0-py3.12-wolfi" in result.output + + +def test_prepare_args_and_stdin_with_api_version() -> None: + """Test prepare_args_and_stdin function with api_version parameter.""" + config_path = pathlib.Path(__file__).parent / "langgraph.json" + config = validate_config( + Config(dependencies=["."], graphs={"agent": "agent.py:graph"}) + ) + port = 8000 + api_version = "0.2.74" + + actual_args, actual_stdin = prepare_args_and_stdin( + capabilities=DEFAULT_DOCKER_CAPABILITIES, + config_path=config_path, + config=config, + docker_compose=None, + port=port, + watch=False, + api_version=api_version, + ) + + expected_args = [ + "--project-directory", + str(pathlib.Path(__file__).parent.absolute()), + "-f", + "-", + ] + + # Check that the args are correct + assert actual_args == expected_args + + # Check that the stdin contains the correct FROM line with api_version + assert f"FROM langchain/langgraph-api:0.2.74-py3.11" in actual_stdin + + +def test_prepare_args_and_stdin_with_api_version_and_image() -> None: + """Test prepare_args_and_stdin function with both api_version and image parameters.""" + config_path = pathlib.Path(__file__).parent / "langgraph.json" + config = validate_config( + Config(dependencies=["."], graphs={"agent": "agent.py:graph"}) + ) + port = 8000 + api_version = "0.2.74" + image = "my-custom-image:latest" + + actual_args, actual_stdin = prepare_args_and_stdin( + capabilities=DEFAULT_DOCKER_CAPABILITIES, + config_path=config_path, + config=config, + docker_compose=None, + port=port, + watch=False, + api_version=api_version, + image=image, + ) + + # When image is provided, api_version should be ignored for the image + # but the stdin should not contain a build section (since image is provided) + assert "pull_policy: build" not in actual_stdin diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index 3971b60e2..f4cf64643 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -1286,3 +1286,193 @@ def test_docker_tag_different_node_versions_with_distro(): ) tag = docker_tag(config) assert tag == expected_tag, f"Failed for Node.js {node_version}" + + +def test_docker_tag_with_api_version(): + """Test docker_tag function with api_version parameter.""" + + # Test 1: Python config with api_version and default distro + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraph-api:0.2.74-py3.11" + + # Test 2: Python config with api_version and wolfi distro + config = validate_config( + { + "python_version": "3.12", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + "image_distro": "wolfi", + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraph-api:0.2.74-py3.12-wolfi" + + # Test 3: Node.js config with api_version and default distro + config = validate_config( + { + "node_version": "20", + "graphs": {"agent": "./agent.js:graph"}, + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraphjs-api:0.2.74-node20" + + # Test 4: Node.js config with api_version and wolfi distro + config = validate_config( + { + "node_version": "20", + "graphs": {"agent": "./agent.js:graph"}, + "image_distro": "wolfi", + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraphjs-api:0.2.74-node20-wolfi" + + # Test 5: Custom base image with api_version + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + "base_image": "my-registry/custom-image", + } + ) + tag = docker_tag(config, base_image="my-registry/custom-image", api_version="1.0.0") + assert tag == "my-registry/custom-image:1.0.0-py3.11" + + # Test 6: api_version with different Python versions + for python_version in ["3.11", "3.12", "3.13"]: + config = validate_config( + { + "python_version": python_version, + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == f"langchain/langgraph-api:0.2.74-py{python_version}" + + # Test 7: Without api_version should work as before + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + tag = docker_tag(config) + assert tag == "langchain/langgraph-api:3.11" + + # Test 8: api_version with multiplatform config (should default to Python) + config = validate_config( + { + "python_version": "3.11", + "node_version": "20", + "dependencies": ["."], + "graphs": {"python": "./agent.py:graph", "js": "./agent.js:graph"}, + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraph-api:0.2.74-py3.11" + + # Test 9: api_version with _INTERNAL_docker_tag should ignore api_version + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + "_INTERNAL_docker_tag": "internal-tag", + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraph-api:internal-tag" + + # Test 10: api_version with langgraph-server base image should follow special format + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + tag = docker_tag(config, base_image="langchain/langgraph-server:0.2", api_version="0.2.74") + assert tag == "langchain/langgraph-server:0.2-py3.11" + + +def test_config_to_docker_with_api_version(): + """Test config_to_docker function with api_version parameter.""" + + # Test Python config with api_version + graphs = {"agent": "./agent.py:graph"} + actual_docker_stdin, additional_contexts = config_to_docker( + PATH_TO_CONFIG, + validate_config({"dependencies": ["."], "graphs": graphs}), + "langchain/langgraph-api", + api_version="0.2.74", + ) + + # Check that the FROM line uses the api_version + lines = actual_docker_stdin.split('\n') + from_line = lines[0] + assert from_line == "FROM langchain/langgraph-api:0.2.74-py3.11" + + # Test Node.js config with api_version + graphs = {"agent": "./agent.js:graph"} + actual_docker_stdin, additional_contexts = config_to_docker( + PATH_TO_CONFIG, + validate_config({"node_version": "20", "graphs": graphs}), + "langchain/langgraphjs-api", + api_version="0.2.74", + ) + + # Check that the FROM line uses the api_version + lines = actual_docker_stdin.split('\n') + from_line = lines[0] + assert from_line == "FROM langchain/langgraphjs-api:0.2.74-node20" + + +def test_config_to_compose_with_api_version(): + """Test config_to_compose function with api_version parameter.""" + + # Test Python config with api_version + config = validate_config( + { + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + + actual_compose_str = config_to_compose( + PATH_TO_CONFIG, + config, + "langchain/langgraph-api", + api_version="0.2.74", + ) + + # Check that the compose file includes the correct FROM line with api_version + assert "FROM langchain/langgraph-api:0.2.74-py3.11" in actual_compose_str + + # Test Node.js config with api_version + config = validate_config( + { + "node_version": "20", + "graphs": {"agent": "./agent.js:graph"}, + } + ) + + actual_compose_str = config_to_compose( + PATH_TO_CONFIG, + config, + "langchain/langgraphjs-api", + api_version="0.2.74", + ) + + # Check that the compose file includes the correct FROM line with api_version + assert "FROM langchain/langgraphjs-api:0.2.74-node20" in actual_compose_str diff --git a/libs/cli/tests/unit_tests/test_docker.py b/libs/cli/tests/unit_tests/test_docker.py index 355efeb4d..0ca2c2a61 100644 --- a/libs/cli/tests/unit_tests/test_docker.py +++ b/libs/cli/tests/unit_tests/test_docker.py @@ -146,3 +146,222 @@ services: REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {DEFAULT_POSTGRES_URI}""" assert clean_empty_lines(actual_compose_str) == expected_compose_str + + +def test_compose_with_api_version(): + """Test compose function with api_version parameter.""" + port = 8123 + api_version = "0.2.74" + + actual_compose_str = compose( + DEFAULT_DOCKER_CAPABILITIES, + port=port, + api_version=api_version + ) + + # The compose function should generate a compose file that doesn't directly + # reference the api_version, since it's handled in the docker tag creation + # when building the image. The compose function mainly sets up services. + 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-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_with_api_version_and_base_image(): + """Test compose function with both api_version and base_image parameters.""" + port = 8123 + api_version = "1.0.0" + base_image = "my-registry/custom-api" + + actual_compose_str = compose( + DEFAULT_DOCKER_CAPABILITIES, + port=port, + api_version=api_version, + base_image=base_image + ) + + # Similar to the previous test - the compose function doesn't directly embed + # the api_version or base_image into the compose file since those are handled + # during the docker build process + 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-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_with_api_version_and_custom_postgres(): + """Test compose function with api_version and custom postgres URI.""" + port = 8123 + api_version = "0.2.74" + custom_postgres_uri = "postgresql://user:pass@external-db:5432/mydb" + + actual_compose_str = compose( + DEFAULT_DOCKER_CAPABILITIES, + port=port, + api_version=api_version, + 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_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