From f4dea9c5f74aa47dae4189a9060083afe960ed8a Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Wed, 28 Aug 2024 02:07:21 +0200 Subject: [PATCH] feat(cli): add building support for JS --- libs/cli/langgraph_cli/cli.py | 24 +++++-- libs/cli/langgraph_cli/config.py | 87 ++++++++++++++++++------ libs/cli/tests/unit_tests/test_config.py | 25 +++++++ 3 files changed, 109 insertions(+), 27 deletions(-) diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index 1e3ffa2a7..ebd7e0701 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -372,7 +372,11 @@ def _build( pull: bool, tag: str, ): - base_image = base_image or "langchain/langgraph-api" + base_image = base_image or ( + "langchain/langgraphjs-api" + if config_json.get("node_version") + else "langchain/langgraph-api" + ) # pull latest images if pull: @@ -380,7 +384,9 @@ def _build( subp_exec( "docker", "pull", - f"{base_image}:{config_json['python_version']}", + f"{base_image}:{config_json['node_version']}" + if config_json.get("node_version") + else f"{base_image}:{config_json['python_version']}", verbose=True, ) ) @@ -462,7 +468,11 @@ def dockerfile(save_path: pathlib.Path, config: pathlib.Path): with open(save_path, "w") as f: f.write( langgraph_cli.config.config_to_docker( - config, config_json, "langchain/langgraph-api" + config, + config_json, + "langchain/langgraphjs-api" + if config_json.get("node_version") + else "langchain/langgraph-api", ) ) @@ -500,7 +510,9 @@ def prepare_args_and_stdin( config_path, config, watch=watch, - base_image="langchain/langgraph-api", + base_image="langchain/langgraphjs-api" + if config.get("node_version") + else "langchain/langgraph-api", ) return args, stdin @@ -527,7 +539,9 @@ def prepare( subp_exec( "docker", "pull", - f"langchain/langgraph-api:{config['python_version']}", + f"langchain/langgraphjs-api:{config['node_version']}" + if config.get("node_version") + else f"langchain/langgraph-api:{config['python_version']}", verbose=verbose, ) ) diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 177125c84..f7b8355d9 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -9,6 +9,7 @@ import click class Config(TypedDict): python_version: str + node_version: Optional[str] pip_config_file: Optional[str] dockerfile_lines: list[str] dependencies: list[str] @@ -17,27 +18,46 @@ class Config(TypedDict): def validate_config(config: Config) -> Config: - config = { - "python_version": config.get("python_version", "3.11"), - "pip_config_file": config.get("pip_config_file"), - "dockerfile_lines": config.get("dockerfile_lines", []), - "dependencies": config.get("dependencies", []), - "graphs": config.get("graphs", {}), - "env": config.get("env", {}), - } - if config["python_version"] not in ( - "3.11", - "3.12", - ): - raise click.UsageError( - f"Unsupported Python version: {config['python_version']}. " - "Supported versions are 3.11 and 3.12." - ) - if not config["dependencies"]: - raise click.UsageError( - "No dependencies found in config. " - "Add at least one dependency to 'dependencies' list." - ) + config = ( + { + "node_version": config.get("node_version"), + "dockerfile_lines": config.get("dockerfile_lines", []), + "graphs": config.get("graphs", {}), + "env": config.get("env", {}), + } + if config.get("node_version") + else { + "python_version": config.get("python_version", "3.11"), + "pip_config_file": config.get("pip_config_file"), + "dockerfile_lines": config.get("dockerfile_lines", []), + "dependencies": config.get("dependencies", []), + "graphs": config.get("graphs", {}), + "env": config.get("env", {}), + } + ) + + if config.get("node_version"): + if config["node_version"] not in ("20"): + raise click.UsageError( + f"Unsupported Node.js version: {config['node_version']}. " + "Currently only `node_version: \"20\"` is supported." + ) + + if config.get("python_version"): + if config["python_version"] not in ( + "3.11", + "3.12", + ): + raise click.UsageError( + f"Unsupported Python version: {config['python_version']}. " + "Supported versions are 3.11 and 3.12." + ) + if not config["dependencies"]: + raise click.UsageError( + "No dependencies found in config. " + "Add at least one dependency to 'dependencies' list." + ) + if not config["graphs"]: raise click.UsageError( "No graphs found in config. " @@ -191,7 +211,7 @@ def _update_graph_paths( config["graphs"][graph_id] = f"{module_str}:{attr_str}" -def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): +def python_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): # configure pip pip_install = ( "PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt" @@ -266,6 +286,29 @@ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}' {f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else ""}""" +def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): + faux_path = f"/deps/{config_path.parent.name}" + + return f"""FROM {base_image}:{config['node_version']} + +{os.linesep.join(config["dockerfile_lines"])} + +ADD . {faux_path} + +RUN cd {faux_path} && yarn install --frozen-lockfile + +ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}' + +WORKDIR {faux_path}""" + + +def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): + if config.get("node_version"): + return node_config_to_docker(config_path, config, base_image) + + return python_config_to_docker(config_path, config, base_image) + + def config_to_compose( config_path: pathlib.Path, config: Config, diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index 870f43faf..f53313986 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -232,6 +232,31 @@ ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'""" assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin +# node.js build used for LangGraph Cloud +def test_config_to_docker_nodejs(): + graphs = {"agent": "./graphs/agent.js:graph"} + actual_docker_stdin = config_to_docker( + PATH_TO_CONFIG, + validate_config( + { + "node_version": "20", + "graphs": graphs, + "dockerfile_lines": ["ARG meow", "ARG foo"], + } + ), + "langchain/langgraphjs-api", + ) + expected_docker_stdin = """FROM langchain/langgraphjs-api:20 +ARG meow +ARG foo +ADD . /deps/unit_tests +RUN cd /deps/unit_tests && yarn install --frozen-lockfile +ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}' +WORKDIR /deps/unit_tests""" + + 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"}