Compare commits

...
Author SHA1 Message Date
William Fu-Hinthorn 8dba54fa0d Add windows CLI testing 2024-11-20 17:34:16 -08:00
Kangxu LiuandGitHub 5247952b31 [CLI] Fix relative path issue on Windows (#2480) 2024-11-20 17:25:43 -08:00
2 changed files with 144 additions and 76 deletions
+60 -2
View File
@@ -14,7 +14,7 @@ jobs:
python-version:
- "3.10"
- "3.11"
name: "CLI integration test"
name: "CLI integration test (Linux)"
defaults:
run:
working-directory: libs/cli
@@ -71,4 +71,62 @@ jobs:
working-directory: libs/cli/js-examples
run: |
langgraph build -t langgraph-test-e
windows-build:
runs-on: windows-latest
name: "CLI integration test (Windows)"
defaults:
run:
working-directory: libs/cli
shell: bash
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
with:
filter: "libs/cli/**"
- name: Set up Python 3.11 + Poetry ${{ env.POETRY_VERSION }}
if: steps.changed-files.outputs.all
uses: "./.github/actions/poetry_setup"
with:
python-version: "3.11"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: integration-test-cli-windows
- name: Setup env
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples
run: cp .env.example .env
- name: Install cli globally
if: steps.changed-files.outputs.all
run: pip install -e .
- name: Build and test service A
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples
run: |
langgraph build -t langgraph-test-a --base-image "langchain/langgraph-trial"
cp .env.example .envg
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -c langgraph.json -t langgraph-test-a
- name: Build and test service B
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs
run: |
langgraph build -t langgraph-test-b --base-image "langchain/langgraph-trial"
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-b
- name: Build and test service C
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_a
run: |
langgraph build -t langgraph-test-c --base-image "langchain/langgraph-trial"
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-c
- name: Build and test service D
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_b
run: |
langgraph build -t langgraph-test-d --base-image "langchain/langgraph-trial"
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-d
- name: Build JS service
if: steps.changed-files.outputs.all
working-directory: libs/cli/js-examples
run: |
langgraph build -t langgraph-test-e
+84 -74
View File
@@ -1,5 +1,4 @@
import json
import os
import pathlib
import textwrap
from typing import NamedTuple, Optional, TypedDict, Union
@@ -192,66 +191,76 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
resolved = config_path.parent / local_dep
# validate local dependency
if not resolved.exists():
raise FileNotFoundError(f"Could not find local dependency: {resolved}")
elif not resolved.is_dir():
raise NotADirectoryError(
f"Local dependency must be a directory: {resolved}"
)
elif not resolved.is_relative_to(config_path.parent):
raise ValueError(
f"Local dependency '{resolved}' must be a subdirectory of '{config_path.parent}'"
)
# if it's installable, add it to local_pkgs
# otherwise, add it to faux_pkgs, and create a pyproject.toml
files = os.listdir(resolved)
if "pyproject.toml" in files:
real_pkgs[resolved] = local_dep
if local_dep == ".":
working_dir = f"/deps/{resolved.name}"
elif "setup.py" in files:
real_pkgs[resolved] = local_dep
if local_dep == ".":
working_dir = f"/deps/{resolved.name}"
else:
if any(file == "__init__.py" for file in files):
# flat layout
if "-" in resolved.name:
raise ValueError(
f"Package name '{resolved.name}' contains a hyphen. "
"Rename the directory to use it as flat-layout package."
)
check_reserved(resolved.name, local_dep)
container_path = f"/deps/__outer_{resolved.name}/{resolved.name}"
else:
# src layout
container_path = f"/deps/__outer_{resolved.name}/src"
for file in files:
rfile = resolved / file
if (
rfile.is_dir()
and file != "__pycache__"
and not file.startswith(".")
):
try:
for subfile in os.listdir(rfile):
if subfile.endswith(".py"):
check_reserved(file, local_dep)
break
except PermissionError:
pass
faux_pkgs[resolved] = (local_dep, container_path)
if local_dep == ".":
working_dir = container_path
if "requirements.txt" in files:
rfile = resolved / "requirements.txt"
pip_reqs.append(
(
rfile.relative_to(config_path.parent),
f"{container_path}/requirements.txt",
)
try:
if not resolved.exists():
raise FileNotFoundError(f"Could not find local dependency: {resolved}")
elif not resolved.is_dir():
raise NotADirectoryError(
f"Local dependency must be a directory: {resolved}"
)
elif not resolved.is_relative_to(config_path.parent):
raise ValueError(
f"Local dependency '{resolved}' must be a subdirectory of '{config_path.parent}'"
)
# if it's installable, add it to local_pkgs
# otherwise, add it to faux_pkgs, and create a pyproject.toml
try:
files = list(resolved.iterdir())
file_names = [f.name for f in files]
except (PermissionError, OSError) as e:
raise click.UsageError(
f"Cannot access directory {resolved}: {str(e)}"
) from None
if "pyproject.toml" in file_names:
real_pkgs[resolved] = local_dep
if local_dep == ".":
working_dir = f"/deps/{resolved.name}"
elif "setup.py" in file_names:
real_pkgs[resolved] = local_dep
if local_dep == ".":
working_dir = f"/deps/{resolved.name}"
else:
if any(file == "__init__.py" for file in file_names):
# flat layout
if "-" in resolved.name:
raise ValueError(
f"Package name '{resolved.name}' contains a hyphen. "
"Rename the directory to use it as flat-layout package."
)
check_reserved(resolved.name, local_dep)
container_path = f"/deps/__outer_{resolved.name}/{resolved.name}"
else:
# src layout
container_path = f"/deps/__outer_{resolved.name}/src"
for file in files:
if (
file.is_dir()
and file.name != "__pycache__"
and not file.name.startswith(".")
):
try:
subfiles = list(file.iterdir())
if any(f.name.endswith(".py") for f in subfiles):
check_reserved(file.name, local_dep)
except (PermissionError, OSError):
continue
faux_pkgs[resolved] = (local_dep, container_path)
if local_dep == ".":
working_dir = container_path
if "requirements.txt" in file_names:
rfile = resolved / "requirements.txt"
pip_reqs.append(
(
pathlib.PurePosixPath(
rfile.relative_to(config_path.parent)
),
f"{container_path}/requirements.txt",
)
)
except (PermissionError, OSError) as e:
raise click.UsageError(f"Cannot access path {resolved}: {str(e)}") from e
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir)
@@ -275,12 +284,12 @@ def _update_graph_paths(
else:
for path in local_deps.real_pkgs:
if resolved.is_relative_to(path):
module_str = f"/deps/{path.name}/{resolved.relative_to(path)}"
module_str = f"/deps/{path.name}/{pathlib.PurePosixPath(resolved.relative_to(path))}"
break
else:
for faux_pkg, (_, destpath) in local_deps.faux_pkgs.items():
if resolved.is_relative_to(faux_pkg):
module_str = f"{destpath}/{resolved.relative_to(faux_pkg)}"
module_str = f"{destpath}/{pathlib.PurePosixPath(resolved.relative_to(faux_pkg))}"
break
else:
raise ValueError(
@@ -313,17 +322,17 @@ def python_config_to_docker(config_path: pathlib.Path, config: Config, base_imag
pip_pkgs_str = f"RUN {pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
if local_deps.pip_reqs:
pip_reqs_str = os.linesep.join(
pip_reqs_str = "\n".join(
f"ADD {reqpath} {destpath}" for reqpath, destpath in local_deps.pip_reqs
)
pip_reqs_str += f'{os.linesep}RUN {pip_install} {" ".join("-r " + r for _,r in local_deps.pip_reqs)}'
pip_reqs_str += (
f'\nRUN {pip_install} {" ".join("-r " + r for _,r in local_deps.pip_reqs)}'
)
else:
pip_reqs_str = ""
# https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data
# https://til.simonwillison.net/python/pyproject
faux_pkgs_str = f"{os.linesep}{os.linesep}".join(
faux_pkgs_str = "\n\n".join(
f"""ADD {relpath} {destpath}
RUN set -ex && \\
for line in '[project]' \\
@@ -335,12 +344,12 @@ RUN set -ex && \\
done"""
for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items()
)
local_pkgs_str = os.linesep.join(
local_pkgs_str = "\n".join(
f"ADD {relpath} /deps/{fullpath.name}"
for fullpath, relpath in local_deps.real_pkgs.items()
)
installs = f"{os.linesep}{os.linesep}".join(
installs = "\n\n".join(
filter(
None,
[
@@ -352,10 +361,11 @@ RUN set -ex && \\
],
)
)
_workdir = f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else ""
dockerfile_lines = "\n".join(config["dockerfile_lines"])
return f"""FROM {base_image}:{config['python_version']}
{os.linesep.join(config["dockerfile_lines"])}
{dockerfile_lines}
{installs}
@@ -363,7 +373,7 @@ RUN {pip_install} -e /deps/*
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
{f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else ""}"""
{_workdir}"""
def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
@@ -390,10 +400,10 @@ def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image:
install_cmd = "npm ci"
else:
install_cmd = "npm i"
dockerfile_lines = "\n".join(config["dockerfile_lines"])
return f"""FROM {base_image}:{config['node_version']}
{os.linesep.join(config["dockerfile_lines"])}
{dockerfile_lines}
ADD . {faux_path}