mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-26 03:25:06 +02:00
Merge branch 'main' into david/30-06/support-base-image-tag
This commit is contained in:
@@ -338,7 +338,10 @@ class HttpConfig(TypedDict, total=False):
|
||||
Default is False.
|
||||
"""
|
||||
disable_meta: bool
|
||||
"""Optional. If True, all meta endpoints (/ok, /info, /metrics, /docs) are disabled.
|
||||
"""Optional. Remove meta endpoints.
|
||||
|
||||
Set to True to disable the following endpoints: /openapi.json, /info, /metrics, /docs.
|
||||
This will also make the /ok endpoint skip any DB or other checks, always returning {"ok": True}.
|
||||
|
||||
Default is False.
|
||||
"""
|
||||
@@ -471,21 +474,61 @@ class Config(TypedDict, total=False):
|
||||
"""Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.
|
||||
"""
|
||||
|
||||
keep_pkg_tools: Optional[Union[bool, list[str]]]
|
||||
"""Optional. Control whether to retain Python packaging tools in the final image.
|
||||
|
||||
Allowed tools are: "pip", "setuptools", "wheel".
|
||||
You can also set to true to include all packaging tools.
|
||||
"""
|
||||
|
||||
PIP_CLEANUP_LINES = """# -- Ensure user deps didn't inadvertently overwrite langgraph-api
|
||||
|
||||
_BUILD_TOOLS = ("pip", "setuptools", "wheel")
|
||||
|
||||
|
||||
def _get_pip_cleanup_lines(
|
||||
install_cmd: str,
|
||||
to_uninstall: Optional[tuple[str]],
|
||||
pip_installer: Literal["uv", "pip"],
|
||||
) -> str:
|
||||
commands = [
|
||||
f"""# -- Ensure user deps didn't inadvertently overwrite langgraph-api
|
||||
RUN mkdir -p /api/langgraph_api /api/langgraph_runtime /api/langgraph_license && \
|
||||
touch /api/langgraph_api/__init__.py /api/langgraph_runtime/__init__.py /api/langgraph_license/__init__.py
|
||||
touch /api/langgraph_api/__init__.py /api/langgraph_runtime/__init__.py /api/langgraph_license/__init__.py
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir --no-deps -e /api
|
||||
# -- End of ensuring user deps didn't inadvertently overwrite langgraph-api --
|
||||
# -- Removing pip from the final image ~<:===~~~ --
|
||||
RUN pip uninstall -y pip setuptools wheel && \
|
||||
rm -rf /usr/local/lib/python*/site-packages/pip* /usr/local/lib/python*/site-packages/setuptools* /usr/local/lib/python*/site-packages/wheel* && \
|
||||
find /usr/local/bin -name "pip*" -delete || true
|
||||
# pip removal for wolfi
|
||||
RUN rm -rf /usr/lib/python*/site-packages/pip* /usr/lib/python*/site-packages/setuptools* /usr/lib/python*/site-packages/wheel* && \
|
||||
find /usr/bin -name "pip*" -delete || true
|
||||
{uv_removal}
|
||||
# -- End of pip removal --"""
|
||||
# -- Removing build deps from the final image ~<:===~~~ --"""
|
||||
]
|
||||
if to_uninstall:
|
||||
for pack in to_uninstall:
|
||||
if pack not in _BUILD_TOOLS:
|
||||
raise ValueError(
|
||||
f"Invalid build tool: {pack}; must be one of {', '.join(_BUILD_TOOLS)}"
|
||||
)
|
||||
packs_str = " ".join(sorted(to_uninstall))
|
||||
commands.append(f"RUN pip uninstall -y {packs_str}")
|
||||
# Ensure the directories are removed entirely
|
||||
packages_rm = " ".join(
|
||||
f"/usr/local/lib/python*/site-packages/{pack}*" for pack in to_uninstall
|
||||
)
|
||||
if "pip" in to_uninstall:
|
||||
packages_rm += ' && find /usr/local/bin -name "pip*" -delete || true'
|
||||
commands.append(f"RUN rm -rf {packages_rm}")
|
||||
wolfi_packages_rm = " ".join(
|
||||
f"/usr/lib/python*/site-packages/{pack}*" for pack in to_uninstall
|
||||
)
|
||||
if "pip" in to_uninstall:
|
||||
wolfi_packages_rm += ' && find /usr/bin -name "pip*" -delete || true'
|
||||
commands.append(f"RUN rm -rf {wolfi_packages_rm}")
|
||||
if pip_installer == "uv":
|
||||
commands.append(
|
||||
f"RUN uv pip uninstall --system {packs_str} && rm /usr/bin/uv /usr/bin/uvx"
|
||||
)
|
||||
else:
|
||||
if pip_installer == "uv":
|
||||
commands.append(
|
||||
"RUN rm /usr/bin/uv /usr/bin/uvx\n# -- End of build deps removal --"
|
||||
)
|
||||
return "\n".join(commands)
|
||||
|
||||
|
||||
def _parse_version(version_str: str) -> tuple[int, int]:
|
||||
@@ -563,6 +606,7 @@ def validate_config(config: Config) -> Config:
|
||||
"checkpointer": config.get("checkpointer"),
|
||||
"ui": config.get("ui"),
|
||||
"ui_config": config.get("ui_config"),
|
||||
"keep_pkg_tools": config.get("keep_pkg_tools"),
|
||||
}
|
||||
|
||||
if config.get("node_version"):
|
||||
@@ -602,8 +646,7 @@ def validate_config(config: Config) -> Config:
|
||||
|
||||
if not config["graphs"]:
|
||||
raise click.UsageError(
|
||||
"No graphs found in config. "
|
||||
"Add at least one graph to 'graphs' dictionary."
|
||||
"No graphs found in config. Add at least one graph to 'graphs' dictionary."
|
||||
)
|
||||
|
||||
# Validate image_distro config
|
||||
@@ -636,6 +679,22 @@ def validate_config(config: Config) -> Config:
|
||||
f"Invalid http.app format: '{http_conf['app']}'. "
|
||||
"Must be in format './path/to/file.py:attribute_name'"
|
||||
)
|
||||
if keep_pkg_tools := config.get("keep_pkg_tools"):
|
||||
if isinstance(keep_pkg_tools, list):
|
||||
for tool in keep_pkg_tools:
|
||||
if tool not in _BUILD_TOOLS:
|
||||
raise ValueError(
|
||||
f"Invalid keep_pkg_tools: '{tool}'. "
|
||||
"Must be one of 'pip', 'setuptools', 'wheel'."
|
||||
)
|
||||
elif keep_pkg_tools is True:
|
||||
pass
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid keep_pkg_tools: '{keep_pkg_tools}'. "
|
||||
"Must be bool or list[str] (with values"
|
||||
" 'pip', 'setuptools', and/or 'wheel')."
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -1129,6 +1188,27 @@ def _image_supports_uv(base_image: str) -> bool:
|
||||
return version >= min_uv
|
||||
|
||||
|
||||
def get_build_tools_to_uninstall(config: Config) -> tuple[str]:
|
||||
keep_pkg_tools = config.get("keep_pkg_tools")
|
||||
if not keep_pkg_tools:
|
||||
return _BUILD_TOOLS
|
||||
if keep_pkg_tools is True:
|
||||
return ()
|
||||
expected = _BUILD_TOOLS
|
||||
if isinstance(keep_pkg_tools, list):
|
||||
for tool in keep_pkg_tools:
|
||||
if tool not in expected:
|
||||
raise ValueError(
|
||||
f"Invalid build tool to uninstall: {tool}. Expected one of {expected}"
|
||||
)
|
||||
return tuple(sorted(set(_BUILD_TOOLS) - set(keep_pkg_tools)))
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid value for keep_pkg_tools: {keep_pkg_tools}."
|
||||
" Expected True or a list containing any of {expected}."
|
||||
)
|
||||
|
||||
|
||||
def python_config_to_docker(
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
@@ -1137,20 +1217,18 @@ def python_config_to_docker(
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
"""Generate a Dockerfile from the configuration."""
|
||||
pip_installer = config.get("pip_installer", "auto")
|
||||
|
||||
build_tools_to_uninstall = get_build_tools_to_uninstall(config)
|
||||
if pip_installer == "auto":
|
||||
if _image_supports_uv(base_image):
|
||||
pip_installer = "uv"
|
||||
else:
|
||||
pip_installer = "pip"
|
||||
if pip_installer == "uv":
|
||||
install_cmd = "uv pip install --system"
|
||||
uv_removal = "RUN uv pip uninstall --system pip setuptools wheel && rm /usr/bin/uv /usr/bin/uvx"
|
||||
elif pip_installer == "pip":
|
||||
install_cmd = "pip install"
|
||||
uv_removal = ""
|
||||
else:
|
||||
if _image_supports_uv(base_image):
|
||||
install_cmd = "uv pip install --system"
|
||||
uv_removal = "RUN uv pip uninstall --system pip setuptools wheel && rm /usr/bin/uv /usr/bin/uvx"
|
||||
else:
|
||||
install_cmd = "pip install"
|
||||
uv_removal = ""
|
||||
raise ValueError(f"Invalid pip_installer: {pip_installer}")
|
||||
|
||||
# configure pip
|
||||
pip_install = f"PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir -c /api/constraints.txt"
|
||||
@@ -1182,7 +1260,7 @@ def python_config_to_docker(
|
||||
)
|
||||
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"{os.linesep}RUN {pip_install} {' '.join('-r ' + r for _, r in local_deps.pip_reqs)}"
|
||||
pip_reqs_str = f"""# -- Installing local requirements --
|
||||
{pip_reqs_str}
|
||||
# -- End of local requirements install --"""
|
||||
@@ -1299,7 +1377,11 @@ ADD {relpath} /deps/{name}
|
||||
js_inst_str,
|
||||
"",
|
||||
# Add pip cleanup after all installations are complete
|
||||
PIP_CLEANUP_LINES.format(install_cmd=install_cmd, uv_removal=uv_removal),
|
||||
_get_pip_cleanup_lines(
|
||||
install_cmd=install_cmd,
|
||||
to_uninstall=build_tools_to_uninstall,
|
||||
pip_installer=pip_installer,
|
||||
),
|
||||
"",
|
||||
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
|
||||
]
|
||||
|
||||
@@ -123,7 +123,7 @@ def _download_repo_with_requests(repo_url: str, path: str) -> None:
|
||||
)
|
||||
except error.HTTPError as e:
|
||||
click.secho(
|
||||
f"❌ Error: Failed to download repository.\n" f"Details: {e}\n",
|
||||
f"❌ Error: Failed to download repository.\nDetails: {e}\n",
|
||||
fg="red",
|
||||
bold=True,
|
||||
err=True,
|
||||
|
||||
Reference in New Issue
Block a user