mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-25 19:15:11 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cedb310a12 |
@@ -158,6 +158,25 @@ OPT_API_VERSION = click.option(
|
|||||||
help="API server version to use for the base image. If unspecified, the latest version will be used.",
|
help="API server version to use for the base image. If unspecified, the latest version will be used.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OPT_TOPOLOGY = click.option(
|
||||||
|
"--topology",
|
||||||
|
type=click.Choice(["single", "split"]),
|
||||||
|
default="split",
|
||||||
|
show_default=True,
|
||||||
|
help="""Deployment topology for the LangGraph services.
|
||||||
|
|
||||||
|
\b
|
||||||
|
- 'single': Run API server and queue worker in the same container (legacy behavior)
|
||||||
|
- 'split': Run API server and queue worker in separate containers (recommended)
|
||||||
|
Other topologies (e.g., 'distributed') coming soon.
|
||||||
|
|
||||||
|
\b
|
||||||
|
Example:
|
||||||
|
langgraph up --topology single
|
||||||
|
\b
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.version_option(version=__version__, prog_name="LangGraph CLI")
|
@click.version_option(version=__version__, prog_name="LangGraph CLI")
|
||||||
@@ -176,6 +195,7 @@ def cli():
|
|||||||
@OPT_WATCH
|
@OPT_WATCH
|
||||||
@OPT_POSTGRES_URI
|
@OPT_POSTGRES_URI
|
||||||
@OPT_API_VERSION
|
@OPT_API_VERSION
|
||||||
|
@OPT_TOPOLOGY
|
||||||
@click.option(
|
@click.option(
|
||||||
"--image",
|
"--image",
|
||||||
type=str,
|
type=str,
|
||||||
@@ -210,6 +230,7 @@ def up(
|
|||||||
debugger_base_url: str | None,
|
debugger_base_url: str | None,
|
||||||
postgres_uri: str | None,
|
postgres_uri: str | None,
|
||||||
api_version: str | None,
|
api_version: str | None,
|
||||||
|
topology: str,
|
||||||
image: str | None,
|
image: str | None,
|
||||||
base_image: str | None,
|
base_image: str | None,
|
||||||
):
|
):
|
||||||
@@ -233,6 +254,7 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
|
|||||||
debugger_base_url=debugger_base_url,
|
debugger_base_url=debugger_base_url,
|
||||||
postgres_uri=postgres_uri,
|
postgres_uri=postgres_uri,
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
|
topology=topology,
|
||||||
image=image,
|
image=image,
|
||||||
base_image=base_image,
|
base_image=base_image,
|
||||||
)
|
)
|
||||||
@@ -557,6 +579,7 @@ def dockerfile(
|
|||||||
capabilities,
|
capabilities,
|
||||||
port=8123,
|
port=8123,
|
||||||
base_image=base_image,
|
base_image=base_image,
|
||||||
|
topology="split",
|
||||||
)
|
)
|
||||||
# Add .env file to the docker-compose.yml for the langgraph-api service
|
# Add .env file to the docker-compose.yml for the langgraph-api service
|
||||||
compose_dict["services"]["langgraph-api"]["env_file"] = [".env"]
|
compose_dict["services"]["langgraph-api"]["env_file"] = [".env"]
|
||||||
@@ -570,6 +593,17 @@ def dockerfile(
|
|||||||
compose_dict["services"]["langgraph-api"]["build"]["args"] = {
|
compose_dict["services"]["langgraph-api"]["build"]["args"] = {
|
||||||
"BASE_IMAGE": base_image
|
"BASE_IMAGE": base_image
|
||||||
}
|
}
|
||||||
|
# Also configure the worker service with the same build context
|
||||||
|
if "langgraph-worker" in compose_dict["services"]:
|
||||||
|
compose_dict["services"]["langgraph-worker"]["env_file"] = [".env"]
|
||||||
|
compose_dict["services"]["langgraph-worker"]["build"] = {
|
||||||
|
"context": ".",
|
||||||
|
"dockerfile": save_path.name,
|
||||||
|
}
|
||||||
|
if base_image:
|
||||||
|
compose_dict["services"]["langgraph-worker"]["build"]["args"] = {
|
||||||
|
"BASE_IMAGE": base_image
|
||||||
|
}
|
||||||
f.write(langgraph_cli.docker.dict_to_yaml(compose_dict))
|
f.write(langgraph_cli.docker.dict_to_yaml(compose_dict))
|
||||||
secho("✅ Created: docker-compose.yml", fg="green")
|
secho("✅ Created: docker-compose.yml", fg="green")
|
||||||
|
|
||||||
@@ -793,6 +827,8 @@ def prepare_args_and_stdin(
|
|||||||
debugger_base_url: str | None = None,
|
debugger_base_url: str | None = None,
|
||||||
postgres_uri: str | None = None,
|
postgres_uri: str | None = None,
|
||||||
api_version: str | None = None,
|
api_version: str | None = None,
|
||||||
|
# Deployment topology: "single" (combined) or "split" (separate API/worker)
|
||||||
|
topology: str = "split",
|
||||||
# Like "my-tag" (if you already built it locally)
|
# Like "my-tag" (if you already built it locally)
|
||||||
image: str | None = None,
|
image: str | None = None,
|
||||||
# Like "langchain/langgraphjs-api" or "langchain/langgraph-api
|
# Like "langchain/langgraphjs-api" or "langchain/langgraph-api
|
||||||
@@ -809,6 +845,7 @@ def prepare_args_and_stdin(
|
|||||||
image=image, # Pass image to compose YAML generator
|
image=image, # Pass image to compose YAML generator
|
||||||
base_image=base_image,
|
base_image=base_image,
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
|
topology=topology,
|
||||||
)
|
)
|
||||||
args = [
|
args = [
|
||||||
"--project-directory",
|
"--project-directory",
|
||||||
@@ -826,6 +863,8 @@ def prepare_args_and_stdin(
|
|||||||
base_image=langgraph_cli.config.default_base_image(config),
|
base_image=langgraph_cli.config.default_base_image(config),
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
image=image,
|
image=image,
|
||||||
|
topology=topology,
|
||||||
|
postgres_uri=postgres_uri,
|
||||||
)
|
)
|
||||||
return args, stdin
|
return args, stdin
|
||||||
|
|
||||||
@@ -844,6 +883,7 @@ def prepare(
|
|||||||
debugger_base_url: str | None = None,
|
debugger_base_url: str | None = None,
|
||||||
postgres_uri: str | None = None,
|
postgres_uri: str | None = None,
|
||||||
api_version: str | None = None,
|
api_version: str | None = None,
|
||||||
|
topology: str = "split",
|
||||||
image: str | None = None,
|
image: str | None = None,
|
||||||
base_image: str | None = None,
|
base_image: str | None = None,
|
||||||
) -> tuple[list[str], str]:
|
) -> tuple[list[str], str]:
|
||||||
@@ -872,6 +912,7 @@ def prepare(
|
|||||||
debugger_base_url=debugger_base_url or f"http://127.0.0.1:{port}",
|
debugger_base_url=debugger_base_url or f"http://127.0.0.1:{port}",
|
||||||
postgres_uri=postgres_uri,
|
postgres_uri=postgres_uri,
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
|
topology=topology,
|
||||||
image=image,
|
image=image,
|
||||||
base_image=base_image,
|
base_image=base_image,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1242,6 +1242,8 @@ def config_to_compose(
|
|||||||
api_version: str | None = None,
|
api_version: str | None = None,
|
||||||
image: str | None = None,
|
image: str | None = None,
|
||||||
watch: bool = False,
|
watch: bool = False,
|
||||||
|
topology: str = "split",
|
||||||
|
postgres_uri: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
base_image = base_image or default_base_image(config)
|
base_image = base_image or default_base_image(config)
|
||||||
|
|
||||||
@@ -1268,11 +1270,20 @@ def config_to_compose(
|
|||||||
else:
|
else:
|
||||||
watch_str = ""
|
watch_str = ""
|
||||||
if image:
|
if image:
|
||||||
return f"""
|
api_config = f"""
|
||||||
{textwrap.indent(env_vars_str, " ")}
|
{textwrap.indent(env_vars_str, " ")}
|
||||||
{env_file_str}
|
{env_file_str}
|
||||||
{watch_str}
|
{watch_str}
|
||||||
"""
|
"""
|
||||||
|
# For split topology with pre-built image, worker also needs env vars
|
||||||
|
if topology == "split":
|
||||||
|
worker_config = f"""
|
||||||
|
langgraph-worker:
|
||||||
|
{textwrap.indent(env_vars_str, " ")}
|
||||||
|
{env_file_str}
|
||||||
|
"""
|
||||||
|
return api_config + worker_config
|
||||||
|
return api_config
|
||||||
|
|
||||||
else:
|
else:
|
||||||
dockerfile, additional_contexts = config_to_docker(
|
dockerfile, additional_contexts = config_to_docker(
|
||||||
@@ -1292,7 +1303,7 @@ def config_to_compose(
|
|||||||
additional_contexts:
|
additional_contexts:
|
||||||
{additional_contexts_str}"""
|
{additional_contexts_str}"""
|
||||||
|
|
||||||
return f"""
|
api_config = f"""
|
||||||
{textwrap.indent(env_vars_str, " ")}
|
{textwrap.indent(env_vars_str, " ")}
|
||||||
{env_file_str}
|
{env_file_str}
|
||||||
pull_policy: build
|
pull_policy: build
|
||||||
@@ -1302,3 +1313,43 @@ def config_to_compose(
|
|||||||
{textwrap.indent(dockerfile, " ")}
|
{textwrap.indent(dockerfile, " ")}
|
||||||
{watch_str}
|
{watch_str}
|
||||||
"""
|
"""
|
||||||
|
# For split topology, add complete worker service definition with build config
|
||||||
|
# (worker service is NOT added in docker.py when building from Dockerfile)
|
||||||
|
if topology == "split":
|
||||||
|
# Import here to avoid circular dependency
|
||||||
|
from langgraph_cli.docker import DEFAULT_POSTGRES_URI
|
||||||
|
|
||||||
|
# Determine if using internal postgres (include_db) based on postgres_uri
|
||||||
|
include_db = postgres_uri is None
|
||||||
|
effective_postgres_uri = postgres_uri or DEFAULT_POSTGRES_URI
|
||||||
|
|
||||||
|
# Build depends_on based on whether we have internal postgres
|
||||||
|
if include_db:
|
||||||
|
worker_depends_on = """depends_on:
|
||||||
|
langgraph-redis:
|
||||||
|
condition: service_healthy
|
||||||
|
langgraph-postgres:
|
||||||
|
condition: service_healthy"""
|
||||||
|
else:
|
||||||
|
worker_depends_on = """depends_on:
|
||||||
|
langgraph-redis:
|
||||||
|
condition: service_healthy"""
|
||||||
|
|
||||||
|
worker_config = f"""
|
||||||
|
langgraph-worker:
|
||||||
|
{worker_depends_on}
|
||||||
|
environment:
|
||||||
|
REDIS_URI: redis://langgraph-redis:6379
|
||||||
|
POSTGRES_URI: {effective_postgres_uri}
|
||||||
|
{textwrap.indent(env_vars_str, " ")}
|
||||||
|
{env_file_str}
|
||||||
|
entrypoint:
|
||||||
|
- /storage/queue_entrypoint.sh
|
||||||
|
pull_policy: build
|
||||||
|
build:
|
||||||
|
context: .{additional_contexts_str}
|
||||||
|
dockerfile_inline: |
|
||||||
|
{textwrap.indent(dockerfile, " ")}
|
||||||
|
"""
|
||||||
|
return api_config + worker_config
|
||||||
|
return api_config
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class Version(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
DockerComposeType = Literal["plugin", "standalone"]
|
DockerComposeType = Literal["plugin", "standalone"]
|
||||||
|
TopologyType = Literal["single", "split"]
|
||||||
|
|
||||||
|
|
||||||
class DockerCapabilities(NamedTuple):
|
class DockerCapabilities(NamedTuple):
|
||||||
@@ -149,6 +150,8 @@ def compose_as_dict(
|
|||||||
base_image: str | None = None,
|
base_image: str | None = None,
|
||||||
# API version of the base image
|
# API version of the base image
|
||||||
api_version: str | None = None,
|
api_version: str | None = None,
|
||||||
|
# Deployment topology: "single" (combined) or "split" (separate API/worker)
|
||||||
|
topology: TopologyType = "split",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Create a docker compose file as a dictionary in YML style."""
|
"""Create a docker compose file as a dictionary in YML style."""
|
||||||
if postgres_uri is None:
|
if postgres_uri is None:
|
||||||
@@ -207,15 +210,20 @@ def compose_as_dict(
|
|||||||
)["langgraph-debugger"]
|
)["langgraph-debugger"]
|
||||||
|
|
||||||
# Add langgraph-api service
|
# Add langgraph-api service
|
||||||
|
api_environment = {
|
||||||
|
"REDIS_URI": "redis://langgraph-redis:6379",
|
||||||
|
"POSTGRES_URI": postgres_uri,
|
||||||
|
}
|
||||||
|
# In split mode, disable queue processing in the API server
|
||||||
|
if topology == "split":
|
||||||
|
api_environment["N_JOBS_PER_WORKER"] = "0"
|
||||||
|
|
||||||
services["langgraph-api"] = {
|
services["langgraph-api"] = {
|
||||||
"ports": [f'"{port}:8000"'],
|
"ports": [f'"{port}:8000"'],
|
||||||
"depends_on": {
|
"depends_on": {
|
||||||
"langgraph-redis": {"condition": "service_healthy"},
|
"langgraph-redis": {"condition": "service_healthy"},
|
||||||
},
|
},
|
||||||
"environment": {
|
"environment": api_environment,
|
||||||
"REDIS_URI": "redis://langgraph-redis:6379",
|
|
||||||
"POSTGRES_URI": postgres_uri,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
if image:
|
if image:
|
||||||
services["langgraph-api"]["image"] = image
|
services["langgraph-api"]["image"] = image
|
||||||
@@ -235,6 +243,37 @@ def compose_as_dict(
|
|||||||
"start_period": "10s",
|
"start_period": "10s",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Add langgraph-worker service in split mode ONLY when using pre-built image
|
||||||
|
# When building from Dockerfile, the worker service is added in config_to_compose
|
||||||
|
# to avoid duplicate service key issues with YAML concatenation
|
||||||
|
if topology == "split" and image:
|
||||||
|
services["langgraph-worker"] = {
|
||||||
|
"depends_on": {
|
||||||
|
"langgraph-redis": {"condition": "service_healthy"},
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"REDIS_URI": "redis://langgraph-redis:6379",
|
||||||
|
"POSTGRES_URI": postgres_uri,
|
||||||
|
},
|
||||||
|
"entrypoint": ["/storage/queue_entrypoint.sh"],
|
||||||
|
"image": image,
|
||||||
|
}
|
||||||
|
|
||||||
|
# If Postgres is included, add it to the dependencies of langgraph-worker
|
||||||
|
if include_db:
|
||||||
|
services["langgraph-worker"]["depends_on"]["langgraph-postgres"] = {
|
||||||
|
"condition": "service_healthy"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Additional healthcheck for langgraph-worker if supported
|
||||||
|
if capabilities.healthcheck_start_interval:
|
||||||
|
services["langgraph-worker"]["healthcheck"] = {
|
||||||
|
"test": "python /api/healthcheck.py",
|
||||||
|
"interval": "60s",
|
||||||
|
"start_interval": "1s",
|
||||||
|
"start_period": "10s",
|
||||||
|
}
|
||||||
|
|
||||||
# Final compose dictionary with volumes included if needed
|
# Final compose dictionary with volumes included if needed
|
||||||
compose_dict = {}
|
compose_dict = {}
|
||||||
if include_db:
|
if include_db:
|
||||||
@@ -255,6 +294,8 @@ def compose(
|
|||||||
image: str | None = None,
|
image: str | None = None,
|
||||||
base_image: str | None = None,
|
base_image: str | None = None,
|
||||||
api_version: str | None = None,
|
api_version: str | None = None,
|
||||||
|
# Deployment topology: "single" (combined) or "split" (separate API/worker)
|
||||||
|
topology: TopologyType = "split",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a docker compose file as a string."""
|
"""Create a docker compose file as a string."""
|
||||||
compose_content = compose_as_dict(
|
compose_content = compose_as_dict(
|
||||||
@@ -266,6 +307,7 @@ def compose(
|
|||||||
image=image,
|
image=image,
|
||||||
base_image=base_image,
|
base_image=base_image,
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
|
topology=topology,
|
||||||
)
|
)
|
||||||
compose_str = dict_to_yaml(compose_content)
|
compose_str = dict_to_yaml(compose_content)
|
||||||
return compose_str
|
return compose_str
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ def test_prepare_args_and_stdin() -> None:
|
|||||||
debugger_port=debugger_port,
|
debugger_port=debugger_port,
|
||||||
debugger_base_url=debugger_graph_url,
|
debugger_base_url=debugger_graph_url,
|
||||||
watch=True,
|
watch=True,
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_args = [
|
expected_args = [
|
||||||
@@ -189,6 +190,7 @@ def test_prepare_args_and_stdin_with_image() -> None:
|
|||||||
debugger_base_url=debugger_graph_url,
|
debugger_base_url=debugger_graph_url,
|
||||||
watch=True,
|
watch=True,
|
||||||
image="my-cool-image",
|
image="my-cool-image",
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_args = [
|
expected_args = [
|
||||||
|
|||||||
@@ -1147,6 +1147,7 @@ def test_config_to_compose_simple_config():
|
|||||||
PATH_TO_CONFIG,
|
PATH_TO_CONFIG,
|
||||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||||
"langchain/langgraph-api",
|
"langchain/langgraph-api",
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
clean_empty_lines(actual_compose_stdin).strip()
|
clean_empty_lines(actual_compose_stdin).strip()
|
||||||
@@ -1195,6 +1196,7 @@ def test_config_to_compose_env_vars():
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
"langchain/langgraph-api-custom",
|
"langchain/langgraph-api-custom",
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
||||||
|
|
||||||
@@ -1233,6 +1235,7 @@ def test_config_to_compose_env_file():
|
|||||||
PATH_TO_CONFIG,
|
PATH_TO_CONFIG,
|
||||||
validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}),
|
validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}),
|
||||||
"langchain/langgraph-api",
|
"langchain/langgraph-api",
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
||||||
|
|
||||||
@@ -1279,6 +1282,7 @@ def test_config_to_compose_watch():
|
|||||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||||
"langchain/langgraph-api",
|
"langchain/langgraph-api",
|
||||||
watch=True,
|
watch=True,
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
||||||
|
|
||||||
@@ -1326,6 +1330,7 @@ def test_config_to_compose_end_to_end():
|
|||||||
validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}),
|
validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}),
|
||||||
"langchain/langgraph-api",
|
"langchain/langgraph-api",
|
||||||
watch=True,
|
watch=True,
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ def test_compose_with_no_debugger_and_custom_db():
|
|||||||
port = 8123
|
port = 8123
|
||||||
custom_postgres_uri = "custom_postgres_uri"
|
custom_postgres_uri = "custom_postgres_uri"
|
||||||
actual_compose_str = compose(
|
actual_compose_str = compose(
|
||||||
DEFAULT_DOCKER_CAPABILITIES, port=port, postgres_uri=custom_postgres_uri
|
DEFAULT_DOCKER_CAPABILITIES,
|
||||||
|
port=port,
|
||||||
|
postgres_uri=custom_postgres_uri,
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
expected_compose_str = f"""services:
|
expected_compose_str = f"""services:
|
||||||
langgraph-redis:
|
langgraph-redis:
|
||||||
@@ -49,6 +52,7 @@ def test_compose_with_no_debugger_and_custom_db_with_healthcheck():
|
|||||||
DEFAULT_DOCKER_CAPABILITIES._replace(healthcheck_start_interval=True),
|
DEFAULT_DOCKER_CAPABILITIES._replace(healthcheck_start_interval=True),
|
||||||
port=port,
|
port=port,
|
||||||
postgres_uri=custom_postgres_uri,
|
postgres_uri=custom_postgres_uri,
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
expected_compose_str = f"""services:
|
expected_compose_str = f"""services:
|
||||||
langgraph-redis:
|
langgraph-redis:
|
||||||
@@ -82,6 +86,7 @@ def test_compose_with_debugger_and_custom_db():
|
|||||||
DEFAULT_DOCKER_CAPABILITIES,
|
DEFAULT_DOCKER_CAPABILITIES,
|
||||||
port=port,
|
port=port,
|
||||||
postgres_uri=custom_postgres_uri,
|
postgres_uri=custom_postgres_uri,
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
expected_compose_str = f"""services:
|
expected_compose_str = f"""services:
|
||||||
langgraph-redis:
|
langgraph-redis:
|
||||||
@@ -105,7 +110,9 @@ def test_compose_with_debugger_and_custom_db():
|
|||||||
|
|
||||||
def test_compose_with_debugger_and_default_db():
|
def test_compose_with_debugger_and_default_db():
|
||||||
port = 8123
|
port = 8123
|
||||||
actual_compose_str = compose(DEFAULT_DOCKER_CAPABILITIES, port=port)
|
actual_compose_str = compose(
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES, port=port, topology="single"
|
||||||
|
)
|
||||||
expected_compose_str = f"""volumes:
|
expected_compose_str = f"""volumes:
|
||||||
langgraph-data:
|
langgraph-data:
|
||||||
driver: local
|
driver: local
|
||||||
@@ -157,7 +164,10 @@ def test_compose_with_api_version():
|
|||||||
api_version = "0.2.74"
|
api_version = "0.2.74"
|
||||||
|
|
||||||
actual_compose_str = compose(
|
actual_compose_str = compose(
|
||||||
DEFAULT_DOCKER_CAPABILITIES, port=port, api_version=api_version
|
DEFAULT_DOCKER_CAPABILITIES,
|
||||||
|
port=port,
|
||||||
|
api_version=api_version,
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
|
|
||||||
# The compose function should generate a compose file that doesn't directly
|
# The compose function should generate a compose file that doesn't directly
|
||||||
@@ -219,6 +229,7 @@ def test_compose_with_api_version_and_base_image():
|
|||||||
port=port,
|
port=port,
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
base_image=base_image,
|
base_image=base_image,
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Similar to the previous test - the compose function doesn't directly embed
|
# Similar to the previous test - the compose function doesn't directly embed
|
||||||
@@ -280,6 +291,7 @@ def test_compose_with_api_version_and_custom_postgres():
|
|||||||
port=port,
|
port=port,
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
postgres_uri=custom_postgres_uri,
|
postgres_uri=custom_postgres_uri,
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_compose_str = f"""services:
|
expected_compose_str = f"""services:
|
||||||
@@ -313,6 +325,7 @@ def test_compose_with_api_version_and_debugger():
|
|||||||
port=port,
|
port=port,
|
||||||
api_version=api_version,
|
api_version=api_version,
|
||||||
debugger_port=debugger_port,
|
debugger_port=debugger_port,
|
||||||
|
topology="single",
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_compose_str = f"""volumes:
|
expected_compose_str = f"""volumes:
|
||||||
@@ -368,6 +381,101 @@ services:
|
|||||||
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_with_single_topology():
|
||||||
|
"""Test compose function with single topology (legacy behavior)."""
|
||||||
|
port = 8123
|
||||||
|
custom_postgres_uri = "custom_postgres_uri"
|
||||||
|
actual_compose_str = compose(
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES,
|
||||||
|
port=port,
|
||||||
|
postgres_uri=custom_postgres_uri,
|
||||||
|
topology="single",
|
||||||
|
)
|
||||||
|
# Assert langgraph-worker service is NOT present
|
||||||
|
assert "langgraph-worker" not in actual_compose_str
|
||||||
|
assert "/storage/queue_entrypoint.sh" not in actual_compose_str
|
||||||
|
# Assert N_JOBS_PER_WORKER is NOT set (queue runs in API container)
|
||||||
|
assert "N_JOBS_PER_WORKER" not in actual_compose_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_with_split_topology():
|
||||||
|
"""Test compose function with split topology when using pre-built image."""
|
||||||
|
port = 8123
|
||||||
|
custom_postgres_uri = "custom_postgres_uri"
|
||||||
|
image = "my-custom-image:latest"
|
||||||
|
actual_compose_str = compose(
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES,
|
||||||
|
port=port,
|
||||||
|
postgres_uri=custom_postgres_uri,
|
||||||
|
topology="split",
|
||||||
|
image=image,
|
||||||
|
)
|
||||||
|
# Assert langgraph-worker service is present when using image
|
||||||
|
assert "langgraph-worker" in actual_compose_str
|
||||||
|
assert "/storage/queue_entrypoint.sh" in actual_compose_str
|
||||||
|
# Assert N_JOBS_PER_WORKER is set to 0 on API service
|
||||||
|
assert "N_JOBS_PER_WORKER" in actual_compose_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_split_topology_is_default():
|
||||||
|
"""Test that split topology is the default (N_JOBS_PER_WORKER set on api)."""
|
||||||
|
port = 8123
|
||||||
|
custom_postgres_uri = "custom_postgres_uri"
|
||||||
|
# Call without specifying topology
|
||||||
|
actual_compose_str = compose(
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES,
|
||||||
|
port=port,
|
||||||
|
postgres_uri=custom_postgres_uri,
|
||||||
|
)
|
||||||
|
# In split mode without image, worker is NOT in compose (added in config_to_compose)
|
||||||
|
# But N_JOBS_PER_WORKER should be set on api service
|
||||||
|
assert "N_JOBS_PER_WORKER" in actual_compose_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_split_topology_with_image():
|
||||||
|
"""Test that worker service uses the same image as api in split mode."""
|
||||||
|
port = 8123
|
||||||
|
image = "my-custom-image:latest"
|
||||||
|
actual_compose_str = compose(
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES,
|
||||||
|
port=port,
|
||||||
|
image=image,
|
||||||
|
topology="split",
|
||||||
|
)
|
||||||
|
# Both services should reference the same image
|
||||||
|
assert actual_compose_str.count(f"image: {image}") == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_split_topology_with_postgres():
|
||||||
|
"""Test split topology includes postgres service (worker added when image provided)."""
|
||||||
|
port = 8123
|
||||||
|
image = "my-custom-image:latest"
|
||||||
|
actual_compose_str = compose(
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES,
|
||||||
|
port=port,
|
||||||
|
topology="split",
|
||||||
|
image=image,
|
||||||
|
)
|
||||||
|
# Worker should be present when image is provided
|
||||||
|
assert "langgraph-worker" in actual_compose_str
|
||||||
|
# The compose should include langgraph-postgres service
|
||||||
|
assert "langgraph-postgres" in actual_compose_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_split_topology_with_healthcheck():
|
||||||
|
"""Test split topology with healthcheck capabilities when using image."""
|
||||||
|
port = 8123
|
||||||
|
image = "my-custom-image:latest"
|
||||||
|
actual_compose_str = compose(
|
||||||
|
DEFAULT_DOCKER_CAPABILITIES._replace(healthcheck_start_interval=True),
|
||||||
|
port=port,
|
||||||
|
topology="split",
|
||||||
|
image=image,
|
||||||
|
)
|
||||||
|
# Both api and worker should have healthchecks
|
||||||
|
assert actual_compose_str.count("python /api/healthcheck.py") == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"input_str,expected",
|
"input_str,expected",
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user