diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 8955ebccd..f68bc4201 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -22,6 +22,24 @@ DEFAULT_IMAGE_DISTRO = "debian" _BUILD_TOOLS = ("pip", "setuptools", "wheel") +def _build_server_config_env(config: Config) -> str: + """Build the AGENT_SERVER_CONFIG env var line from a validated config.""" + server_config: dict = {"graphs": config["graphs"]} + for key in ( + "store", + "auth", + "encryption", + "http", + "webhooks", + "checkpointer", + "ui", + "ui_config", + ): + if (value := config.get(key)) is not None: + server_config[key] = value + return f"ENV AGENT_SERVER_CONFIG='{json.dumps(server_config)}'" + + def _get_pip_cleanup_lines( install_cmd: str, to_uninstall: tuple[str] | None, @@ -1006,36 +1024,7 @@ ADD {relpath} /deps/{name} ) ) - env_vars = [] - - if (store_config := config.get("store")) is not None: - env_vars.append(f"ENV LANGGRAPH_STORE='{json.dumps(store_config)}'") - - if (auth_config := config.get("auth")) is not None: - env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'") - - if (encryption_config := config.get("encryption")) is not None: - env_vars.append(f"ENV LANGGRAPH_ENCRYPTION='{json.dumps(encryption_config)}'") - - if (http_config := config.get("http")) is not None: - env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'") - - # Inject webhooks configuration if provided - if (webhooks_config := config.get("webhooks")) is not None: - env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'") - - if (checkpointer_config := config.get("checkpointer")) is not None: - env_vars.append( - f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'" - ) - - if (ui := config.get("ui")) is not None: - env_vars.append(f"ENV LANGGRAPH_UI='{json.dumps(ui)}'") - - if (ui_config := config.get("ui_config")) is not None: - env_vars.append(f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(ui_config)}'") - - env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'") + env_vars = [_build_server_config_env(config)] js_inst_str: str = "" if (config.get("ui") or config.get("node_version")) and local_deps.working_dir: @@ -1137,36 +1126,7 @@ def node_config_to_docker( image_str = docker_tag(config, base_image, api_version) - env_vars: list[str] = [] - - if (store_config := config.get("store")) is not None: - env_vars.append(f"ENV LANGGRAPH_STORE='{json.dumps(store_config)}'") - - if (auth_config := config.get("auth")) is not None: - env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'") - - if (encryption_config := config.get("encryption")) is not None: - env_vars.append(f"ENV LANGGRAPH_ENCRYPTION='{json.dumps(encryption_config)}'") - - if (http_config := config.get("http")) is not None: - env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'") - - # Inject webhooks configuration if provided - if (webhooks_config := config.get("webhooks")) is not None: - env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'") - - if (checkpointer_config := config.get("checkpointer")) is not None: - env_vars.append( - f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'" - ) - - if ui := config.get("ui"): - env_vars.append(f"ENV LANGGRAPH_UI='{json.dumps(ui)}'") - - if ui_config := config.get("ui_config"): - env_vars.append(f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(ui_config)}'") - - env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'") + env_vars: list[str] = [_build_server_config_env(config)] # For monorepo support, we need to handle install and build commands differently if build_context: diff --git a/libs/cli/langgraph_cli/schemas.py b/libs/cli/langgraph_cli/schemas.py index 011ce4752..9d2fa0310 100644 --- a/libs/cli/langgraph_cli/schemas.py +++ b/libs/cli/langgraph_cli/schemas.py @@ -713,9 +713,28 @@ class Config(TypedDict, total=False): """ +class ServerConfig(TypedDict, total=False): + """Unified server configuration emitted as ``AGENT_SERVER_CONFIG``. + + Mirrors the server-side ``ServerConfig`` but only contains fields + relevant to the CLI's Dockerfile / docker-compose emission. + """ + + graphs: dict[str, str] + store: StoreConfig | None + auth: AuthConfig | None + encryption: EncryptionConfig | None + http: HttpConfig | None + webhooks: WebhooksConfig | None + checkpointer: CheckpointerConfig | None + ui: dict[str, str] | None + ui_config: dict | None + + __all__ = [ "Config", "GraphDef", + "ServerConfig", "StoreConfig", "CheckpointerConfig", "AuthConfig", diff --git a/libs/cli/tests/unit_tests/cli/test_cli.py b/libs/cli/tests/unit_tests/cli/test_cli.py index 58ef68343..01c24e17b 100644 --- a/libs/cli/tests/unit_tests/cli/test_cli.py +++ b/libs/cli/tests/unit_tests/cli/test_cli.py @@ -152,7 +152,7 @@ services: # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- - ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}' + ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "agent.py:graph"}}}}' {textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")} WORKDIR /deps/cli diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index eecc887ed..899315c65 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -474,8 +474,7 @@ RUN set -ex && \\ # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- -ENV LANGGRAPH_HTTP='{{"app": "/deps/examples/my_app.py:app"}}' -ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}' +ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}, "http": {{"app": "/deps/examples/my_app.py:app"}}}}' {FORMATTED_CLEANUP_LINES} WORKDIR /deps/outer-unit_tests/unit_tests\ """ @@ -531,7 +530,7 @@ RUN set -ex && \\ # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- -ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}' +ENV AGENT_SERVER_CONFIG='{"graphs": {"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}' """ + FORMATTED_CLEANUP_LINES + """ @@ -578,7 +577,7 @@ RUN set -ex && \\ # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- -ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}' +ENV AGENT_SERVER_CONFIG='{"graphs": {"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}' """ + FORMATTED_CLEANUP_LINES + """ @@ -640,7 +639,7 @@ RUN set -ex && \\ # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- -ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-graphs/src/agent.py:graph"}}' +ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-graphs/src/agent.py:graph"}}}}' {FORMATTED_CLEANUP_LINES}\ """ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin @@ -676,7 +675,7 @@ ADD . /deps/unit_tests # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- -ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}' +ENV AGENT_SERVER_CONFIG='{"graphs": {"agent": "/deps/unit_tests/graphs/agent.py:graph"}}' """ + FORMATTED_CLEANUP_LINES + "\n" @@ -724,7 +723,7 @@ RUN set -ex && \\ # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- -ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-graphs/src/agent.py:graph"}}' +ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-graphs/src/agent.py:graph"}}}}' {FORMATTED_CLEANUP_LINES}""" assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin assert additional_contexts == {} @@ -752,10 +751,7 @@ ARG meow ARG foo ADD . /deps/unit_tests RUN cd /deps/unit_tests && npm i -ENV LANGGRAPH_AUTH='{"path": "./graphs/auth.mts:auth"}' -ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}' -ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}' -ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}' +ENV AGENT_SERVER_CONFIG='{"graphs": {"agent": "./graphs/agent.js:graph"}, "auth": {"path": "./graphs/auth.mts:auth"}, "ui": {"agent": "./graphs/agent.ui.jsx"}, "ui_config": {"shared": ["nuqs"]}}' WORKDIR /deps/unit_tests RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts""" @@ -809,8 +805,8 @@ def test_config_to_docker_python_encryption_formatted(): ), base_image="langchain/langgraph-api", ) - # Verify that LANGGRAPH_ENCRYPTION is in the docker output with the correct path - assert "LANGGRAPH_ENCRYPTION=" in actual_docker_stdin + # Verify that AGENT_SERVER_CONFIG contains encryption with the correct path + assert "AGENT_SERVER_CONFIG=" in actual_docker_stdin assert ( "/deps/outer-unit_tests/unit_tests/agent.py:my_encryption" in actual_docker_stdin @@ -839,10 +835,7 @@ ARG meow ARG foo ADD . /deps/unit_tests RUN cd /deps/unit_tests && npm i -ENV LANGGRAPH_AUTH='{"path": "./graphs/auth.mts:auth"}' -ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}' -ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}' -ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}' +ENV AGENT_SERVER_CONFIG='{"graphs": {"agent": "./graphs/agent.js:graph"}, "auth": {"path": "./graphs/auth.mts:auth"}, "ui": {"agent": "./graphs/agent.ui.jsx"}, "ui_config": {"shared": ["nuqs"]}}' WORKDIR /deps/unit_tests RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts""" @@ -850,14 +843,14 @@ RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not foun assert additional_contexts == {} -def _extract_env_json(dockerfile: str, var_name: str) -> dict: - """Helper to extract and parse a JSON value from an ENV line in a Dockerfile.""" - line_prefix = f"ENV {var_name}='" +def _extract_server_config(dockerfile: str) -> dict: + """Helper to extract and parse the AGENT_SERVER_CONFIG JSON from a Dockerfile.""" + line_prefix = "ENV AGENT_SERVER_CONFIG='" for line in dockerfile.splitlines(): if line.startswith(line_prefix) and line.endswith("'"): json_str = line[len(line_prefix) : -1] return json.loads(json_str) - raise AssertionError(f"{var_name} not found in Dockerfile env lines") + raise AssertionError("AGENT_SERVER_CONFIG not found in Dockerfile env lines") def test_config_to_docker_webhooks_python(): @@ -890,8 +883,8 @@ def test_config_to_docker_webhooks_python(): ) # Ensure the ENV line is present and the payload round-trips via JSON - parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS") - assert parsed == webhooks + server_config = _extract_server_config(dockerfile) + assert server_config["webhooks"] == webhooks def test_config_to_docker_webhooks_node(): @@ -914,8 +907,8 @@ def test_config_to_docker_webhooks_node(): base_image="langchain/langgraphjs-api", ) - parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS") - assert parsed == webhooks + server_config = _extract_server_config(dockerfile) + assert server_config["webhooks"] == webhooks def test_config_to_docker_no_webhooks(): @@ -926,7 +919,8 @@ def test_config_to_docker_no_webhooks(): base_image="langchain/langgraph-api", ) - assert "ENV LANGGRAPH_WEBHOOKS=" not in dockerfile + server_config = _extract_server_config(dockerfile) + assert "webhooks" not in server_config def test_config_to_docker_gen_ui_python(): @@ -963,9 +957,7 @@ RUN set -ex && \\ # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- -ENV LANGGRAPH_UI='{{"agent": "./graphs/agent.ui.jsx"}}' -ENV LANGGRAPH_UI_CONFIG='{{"shared": ["nuqs"]}}' -ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}' +ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}, "ui": {{"agent": "./graphs/agent.ui.jsx"}}, "ui_config": {{"shared": ["nuqs"]}}}}' # -- Installing JS dependencies -- ENV NODE_VERSION=20 RUN cd /deps/outer-unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts @@ -1009,7 +1001,7 @@ RUN set -ex && \\ # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- -ENV LANGSERVE_GRAPHS='{{"python": "/deps/outer-unit_tests/unit_tests/multiplatform/python.py:graph", "js": "/deps/outer-unit_tests/unit_tests/multiplatform/js.mts:graph"}}' +ENV AGENT_SERVER_CONFIG='{{"graphs": {{"python": "/deps/outer-unit_tests/unit_tests/multiplatform/python.py:graph", "js": "/deps/outer-unit_tests/unit_tests/multiplatform/js.mts:graph"}}}}' # -- Installing JS dependencies -- ENV NODE_VERSION=22 RUN cd /deps/outer-unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts @@ -1150,7 +1142,7 @@ def test_config_to_compose_simple_config(): # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- - ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}' + ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}}}' {textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")} WORKDIR /deps/outer-unit_tests/unit_tests """ @@ -1191,7 +1183,7 @@ def test_config_to_compose_env_vars(): # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- - ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}' + ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}}}' {textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")} WORKDIR /deps/outer-unit_tests/unit_tests """ @@ -1236,7 +1228,7 @@ def test_config_to_compose_env_file(): # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- - ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}' + ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}}}' {textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")} WORKDIR /deps/outer-unit_tests/unit_tests """ @@ -1274,7 +1266,7 @@ def test_config_to_compose_watch(): # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- - ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}' + ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}}}' {textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")} WORKDIR /deps/outer-unit_tests/unit_tests @@ -1321,7 +1313,7 @@ def test_config_to_compose_end_to_end(): # -- Installing all local dependencies -- RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done # -- End of local dependencies install -- - ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}' + ENV AGENT_SERVER_CONFIG='{{"graphs": {{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}}}' {textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")} WORKDIR /deps/outer-unit_tests/unit_tests