mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-10 11:47:51 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dba54fa0d | ||
|
|
5247952b31 |
@@ -14,7 +14,7 @@ jobs:
|
|||||||
python-version:
|
python-version:
|
||||||
- "3.10"
|
- "3.10"
|
||||||
- "3.11"
|
- "3.11"
|
||||||
name: "CLI integration test"
|
name: "CLI integration test (Linux)"
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: libs/cli
|
working-directory: libs/cli
|
||||||
@@ -71,4 +71,62 @@ jobs:
|
|||||||
working-directory: libs/cli/js-examples
|
working-directory: libs/cli/js-examples
|
||||||
run: |
|
run: |
|
||||||
langgraph build -t langgraph-test-e
|
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
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import NamedTuple, Optional, TypedDict, Union
|
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
|
resolved = config_path.parent / local_dep
|
||||||
|
|
||||||
# validate local dependency
|
# validate local dependency
|
||||||
if not resolved.exists():
|
try:
|
||||||
raise FileNotFoundError(f"Could not find local dependency: {resolved}")
|
if not resolved.exists():
|
||||||
elif not resolved.is_dir():
|
raise FileNotFoundError(f"Could not find local dependency: {resolved}")
|
||||||
raise NotADirectoryError(
|
elif not resolved.is_dir():
|
||||||
f"Local dependency must be a directory: {resolved}"
|
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",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
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)
|
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir)
|
||||||
|
|
||||||
@@ -275,12 +284,12 @@ def _update_graph_paths(
|
|||||||
else:
|
else:
|
||||||
for path in local_deps.real_pkgs:
|
for path in local_deps.real_pkgs:
|
||||||
if resolved.is_relative_to(path):
|
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
|
break
|
||||||
else:
|
else:
|
||||||
for faux_pkg, (_, destpath) in local_deps.faux_pkgs.items():
|
for faux_pkg, (_, destpath) in local_deps.faux_pkgs.items():
|
||||||
if resolved.is_relative_to(faux_pkg):
|
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
|
break
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
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 ""
|
pip_pkgs_str = f"RUN {pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
|
||||||
if local_deps.pip_reqs:
|
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
|
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:
|
else:
|
||||||
pip_reqs_str = ""
|
pip_reqs_str = ""
|
||||||
|
|
||||||
# https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data
|
# https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data
|
||||||
# https://til.simonwillison.net/python/pyproject
|
# 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}
|
f"""ADD {relpath} {destpath}
|
||||||
RUN set -ex && \\
|
RUN set -ex && \\
|
||||||
for line in '[project]' \\
|
for line in '[project]' \\
|
||||||
@@ -335,12 +344,12 @@ RUN set -ex && \\
|
|||||||
done"""
|
done"""
|
||||||
for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items()
|
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}"
|
f"ADD {relpath} /deps/{fullpath.name}"
|
||||||
for fullpath, relpath in local_deps.real_pkgs.items()
|
for fullpath, relpath in local_deps.real_pkgs.items()
|
||||||
)
|
)
|
||||||
|
|
||||||
installs = f"{os.linesep}{os.linesep}".join(
|
installs = "\n\n".join(
|
||||||
filter(
|
filter(
|
||||||
None,
|
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']}
|
return f"""FROM {base_image}:{config['python_version']}
|
||||||
|
|
||||||
{os.linesep.join(config["dockerfile_lines"])}
|
{dockerfile_lines}
|
||||||
|
|
||||||
{installs}
|
{installs}
|
||||||
|
|
||||||
@@ -363,7 +373,7 @@ RUN {pip_install} -e /deps/*
|
|||||||
|
|
||||||
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
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):
|
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"
|
install_cmd = "npm ci"
|
||||||
else:
|
else:
|
||||||
install_cmd = "npm i"
|
install_cmd = "npm i"
|
||||||
|
dockerfile_lines = "\n".join(config["dockerfile_lines"])
|
||||||
return f"""FROM {base_image}:{config['node_version']}
|
return f"""FROM {base_image}:{config['node_version']}
|
||||||
|
|
||||||
{os.linesep.join(config["dockerfile_lines"])}
|
{dockerfile_lines}
|
||||||
|
|
||||||
ADD . {faux_path}
|
ADD . {faux_path}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user