mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-11-04 21:31:21 +01:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd2d92c31a | ||
|
|
5db97b9cca | ||
|
|
3c916d7505 |
No files matched your search
@@ -154,12 +154,9 @@ def _get_pip_cleanup_lines(
|
||||
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
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir --no-deps -e /api
|
||||
# -- End of ensuring user deps didn't inadvertently overwrite langgraph-api --
|
||||
# -- Removing build deps from the final image ~<:===~~~ --"""
|
||||
"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",
|
||||
f"PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir --no-deps -e /api",
|
||||
]
|
||||
if to_uninstall:
|
||||
for pack in to_uninstall:
|
||||
@@ -168,30 +165,31 @@ RUN PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir --no-deps -e /api
|
||||
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}")
|
||||
commands.append(f"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}")
|
||||
commands.append(f"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}")
|
||||
commands.append(f"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"
|
||||
f"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)
|
||||
elif pip_installer == "uv":
|
||||
commands.append("rm /usr/bin/uv /usr/bin/uvx")
|
||||
# One RUN, since each layer is costly on copy-based snapshotters; subshells scope each `|| true`.
|
||||
return (
|
||||
"# -- Restore langgraph-api over user deps, then remove build deps --\n"
|
||||
"RUN " + " && \\\n ".join(f"({command})" for command in commands)
|
||||
)
|
||||
|
||||
|
||||
def _parse_version(version_str: str) -> tuple[int, int]:
|
||||
@@ -1608,7 +1606,10 @@ ADD {relpath} /deps/{name}
|
||||
pip_installer=pip_installer,
|
||||
),
|
||||
"",
|
||||
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
|
||||
# The JS install step already set this WORKDIR.
|
||||
f"WORKDIR {local_deps.working_dir}"
|
||||
if local_deps.working_dir and not js_inst_str
|
||||
else "",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1690,7 +1691,7 @@ def node_config_to_docker(
|
||||
"",
|
||||
os.linesep.join(env_vars),
|
||||
"",
|
||||
f"WORKDIR {build_workdir}",
|
||||
f"WORKDIR {build_workdir}" if build_workdir != install_workdir else "",
|
||||
"",
|
||||
build_step,
|
||||
]
|
||||
|
||||
@@ -65,6 +65,7 @@ class UvLockPlan:
|
||||
@dataclass(slots=True)
|
||||
class DockerBuildPlan:
|
||||
lines: list[str]
|
||||
workdir: str | None = None
|
||||
|
||||
def add_blank(self) -> None:
|
||||
self.lines.append("")
|
||||
@@ -73,6 +74,11 @@ class DockerBuildPlan:
|
||||
self.lines.append(line)
|
||||
|
||||
def add_instruction(self, opcode: str, value: str | None = None) -> None:
|
||||
# Repeating the current WORKDIR changes nothing but still costs a build step.
|
||||
if opcode == "WORKDIR":
|
||||
if value == self.workdir:
|
||||
return
|
||||
self.workdir = value
|
||||
if value:
|
||||
self.lines.append(f"{opcode} {value}")
|
||||
else:
|
||||
|
||||
@@ -1231,7 +1231,6 @@ 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"}'
|
||||
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"""
|
||||
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
@@ -1422,7 +1421,6 @@ 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"}'
|
||||
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"""
|
||||
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
@@ -1550,8 +1548,7 @@ ENV NODE_VERSION=20
|
||||
WORKDIR /deps/outer-unit_tests/unit_tests
|
||||
RUN npm i && tsx /api/langgraph_api/js/build.mts
|
||||
# -- End of JS dependencies install --
|
||||
{FORMATTED_CLEANUP_LINES}
|
||||
WORKDIR /deps/outer-unit_tests/unit_tests"""
|
||||
{FORMATTED_CLEANUP_LINES}"""
|
||||
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
@@ -1595,8 +1592,7 @@ ENV NODE_VERSION=22
|
||||
WORKDIR /deps/outer-unit_tests/unit_tests
|
||||
RUN npm i && tsx /api/langgraph_api/js/build.mts
|
||||
# -- End of JS dependencies install --
|
||||
{FORMATTED_CLEANUP_LINES}
|
||||
WORKDIR /deps/outer-unit_tests/unit_tests"""
|
||||
{FORMATTED_CLEANUP_LINES}"""
|
||||
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
@@ -2152,7 +2148,8 @@ def test_config_to_docker_uv_lock_detects_js_pm_from_target_package_root():
|
||||
config_path, config, base_image="langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
|
||||
assert "WORKDIR /deps/workspace/apps/agent" in docker
|
||||
# Package install, JS install and runtime all share this dir: one WORKDIR.
|
||||
assert docker.splitlines().count("WORKDIR /deps/workspace/apps/agent") == 1
|
||||
assert (
|
||||
"RUN pnpm i --frozen-lockfile && tsx /api/langgraph_api/js/build.mts"
|
||||
in docker
|
||||
@@ -2803,7 +2800,7 @@ def test_config_retain_build_tools():
|
||||
"/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_true
|
||||
for pckg in _BUILD_TOOLS
|
||||
)
|
||||
assert "RUN pip uninstall -y pip setuptools wheel" not in docker_true
|
||||
assert "(pip uninstall -y pip setuptools wheel)" not in docker_true
|
||||
config_false = validate_config(
|
||||
{**copy.deepcopy(base_config), "keep_pkg_tools": False}
|
||||
)
|
||||
@@ -2814,7 +2811,7 @@ def test_config_retain_build_tools():
|
||||
"/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_false
|
||||
for pckg in _BUILD_TOOLS
|
||||
)
|
||||
assert "RUN pip uninstall -y pip setuptools wheel" in docker_false
|
||||
assert "(pip uninstall -y pip setuptools wheel)" in docker_false
|
||||
config_list = validate_config(
|
||||
{**copy.deepcopy(base_config), "keep_pkg_tools": ["pip", "setuptools"]}
|
||||
)
|
||||
@@ -2829,8 +2826,15 @@ def test_config_retain_build_tools():
|
||||
"/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_list
|
||||
for pckg in ("pip", "setuptools")
|
||||
)
|
||||
assert "RUN pip uninstall -y wheel" in docker_list
|
||||
assert "RUN pip uninstall -y pip setuptools" not in docker_list
|
||||
assert "(pip uninstall -y wheel)" in docker_list
|
||||
assert "(pip uninstall -y pip setuptools)" not in docker_list
|
||||
|
||||
|
||||
def test_pip_cleanup_is_a_single_run_with_scoped_fallbacks():
|
||||
cleanup = _get_pip_cleanup_lines("uv pip install --system", _BUILD_TOOLS, "uv")
|
||||
assert cleanup.count("RUN ") == 1
|
||||
# Each `|| true` must close its own subshell so it can't mask later steps.
|
||||
assert cleanup.count("|| true)") == cleanup.count("|| true") == 2
|
||||
|
||||
|
||||
# config_to_compose
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## 0.4.12
|
||||
|
||||
### Removed
|
||||
|
||||
- **`PrioritySettings.fairness_weight`** — removed. The server stores only
|
||||
`priority` and `fairness_group` on the thread; it ignores a
|
||||
`fairness_weight` and logs a warning. Remove it from your callback.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`PrioritySettings.priority`** — now says -5 runs first and 5 runs last,
|
||||
matching how the server orders runs. 0.4.9 to 0.4.11 said the reverse, and
|
||||
their example gave premium users 3, which runs them after everyone at the
|
||||
default 0. Check any callback written from those docs.
|
||||
|
||||
## 0.4.11 and earlier
|
||||
|
||||
### Added
|
||||
|
||||
@@ -38,7 +53,7 @@
|
||||
and name the object in the `priority` section of `langgraph.json`. The
|
||||
server calls the function with a `PriorityContext` at the first run on a
|
||||
thread with no settings and stores the `PrioritySettings` it returns
|
||||
(`priority`, `fairness_group`, `fairness_weight`) on the thread. Requires
|
||||
(`priority`, `fairness_group`) on the thread. Requires
|
||||
`langgraph-api` 0.18.0 or later and a `langgraph-cli` that supports the
|
||||
section; older versions of either ignore it.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from langgraph_sdk.encryption.types import DecryptResult, EncryptionContext
|
||||
from langgraph_sdk.priority import Priority
|
||||
from langgraph_sdk.priority.types import PriorityContext, PrioritySettings
|
||||
|
||||
__version__ = "0.4.11"
|
||||
__version__ = "0.4.12"
|
||||
|
||||
__all__ = [
|
||||
"Auth",
|
||||
|
||||
@@ -102,7 +102,7 @@ class Priority:
|
||||
@priority.assign
|
||||
async def assign(ctx: PriorityContext) -> PrioritySettings | None:
|
||||
return {
|
||||
"priority": 3 if "premium" in ctx.permissions else 0,
|
||||
"priority": -3 if "premium" in ctx.permissions else 0,
|
||||
# Stored unencrypted, so an opaque ID rather than personal data.
|
||||
"fairness_group": ctx.user["org_id"],
|
||||
}
|
||||
@@ -134,7 +134,7 @@ class Priority:
|
||||
@priority.assign
|
||||
async def assign(ctx: PriorityContext) -> PrioritySettings | None:
|
||||
if ctx.cron_id is not None:
|
||||
return {"priority": -2}
|
||||
return {"priority": 2}
|
||||
return None
|
||||
```
|
||||
|
||||
|
||||
@@ -50,18 +50,20 @@ class PrioritySettings(typing.TypedDict, total=False):
|
||||
|
||||
Every key is optional: a missing key or a key set to `None` takes its
|
||||
default, and returning `None` instead of a dict gives every key its default.
|
||||
A value outside its range, or an empty `fairness_group`, is an error.
|
||||
A `priority` outside -5 to 5, or a `fairness_group` longer than 256
|
||||
characters, is an error.
|
||||
"""
|
||||
|
||||
priority: int | None
|
||||
"""An integer from -5 to 5, where 5 runs first and -5 runs last. Defaults to 0."""
|
||||
"""An integer from -5 to 5, where -5 runs first and 5 runs last. Defaults to 0."""
|
||||
|
||||
fairness_group: str | None
|
||||
"""The thread's fairness group, a non-empty string. Defaults to `"default"`.
|
||||
"""Currently disabled.
|
||||
The thread's fairness group, a string of at most 256 characters.
|
||||
|
||||
Defaults to `"default"`, as does an empty string. It has no effect on run
|
||||
order for now.
|
||||
|
||||
The server stores it unencrypted, even when the deployment uses custom
|
||||
encryption, so use an opaque ID rather than personal data.
|
||||
"""
|
||||
|
||||
fairness_weight: int | None
|
||||
"""The weight of the thread's fairness group, an integer from 1 to 100. Defaults to 1."""
|
||||
@@ -120,9 +120,5 @@ def test_context_is_frozen():
|
||||
|
||||
|
||||
def test_every_settings_key_is_optional():
|
||||
assert PrioritySettings.__optional_keys__ == {
|
||||
"priority",
|
||||
"fairness_group",
|
||||
"fairness_weight",
|
||||
}
|
||||
assert PrioritySettings.__optional_keys__ == {"priority", "fairness_group"}
|
||||
assert PrioritySettings.__required_keys__ == frozenset()
|
||||
Reference in new issue
Block a user