mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-09 19:27:54 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8699989b1a | ||
|
|
468c7546c8 |
@@ -287,6 +287,18 @@ def validate_config(config: Config) -> Config:
|
|||||||
f"Invalid http.app format: '{http_conf['app']}'. "
|
f"Invalid http.app format: '{http_conf['app']}'. "
|
||||||
"Must be in format './path/to/file.py:attribute_name'"
|
"Must be in format './path/to/file.py:attribute_name'"
|
||||||
)
|
)
|
||||||
|
if "apps" in http_conf and http_conf["apps"]:
|
||||||
|
for prefix, app_path in http_conf["apps"].items():
|
||||||
|
if not prefix.startswith("/"):
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid http.apps prefix: '{prefix}'. Must start with '/'"
|
||||||
|
)
|
||||||
|
if ":" not in app_path:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid http.apps format for prefix '{prefix}': '{app_path}'. "
|
||||||
|
"Must be in format './path/to/file:attribute_name' "
|
||||||
|
"or 'package:attribute_name'"
|
||||||
|
)
|
||||||
if keep_pkg_tools := config.get("keep_pkg_tools"):
|
if keep_pkg_tools := config.get("keep_pkg_tools"):
|
||||||
if isinstance(keep_pkg_tools, list):
|
if isinstance(keep_pkg_tools, list):
|
||||||
for tool in keep_pkg_tools:
|
for tool in keep_pkg_tools:
|
||||||
@@ -807,6 +819,94 @@ def _update_http_app_path(
|
|||||||
http_config["app"] = f"{module_str}:{attr_str}"
|
http_config["app"] = f"{module_str}:{attr_str}"
|
||||||
|
|
||||||
|
|
||||||
|
_JS_EXTENSIONS = frozenset({".js", ".ts", ".mts", ".mjs", ".cts", ".cjs"})
|
||||||
|
|
||||||
|
|
||||||
|
def _is_js_http_app(app_path: str) -> bool:
|
||||||
|
"""Check if an http.apps entry is a JavaScript/TypeScript app or npm package."""
|
||||||
|
module_str = app_path.split(":")[0]
|
||||||
|
ext = os.path.splitext(module_str)[1]
|
||||||
|
if ext:
|
||||||
|
return ext in _JS_EXTENSIONS
|
||||||
|
# No extension and not a file path → npm package reference (JS)
|
||||||
|
if not module_str.startswith((".", "/")):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _has_js_http_apps(config: Config) -> bool:
|
||||||
|
"""Check if any http.apps entries are JavaScript apps."""
|
||||||
|
http_config = config.get("http")
|
||||||
|
if not http_config:
|
||||||
|
return False
|
||||||
|
apps = http_config.get("apps")
|
||||||
|
if not apps:
|
||||||
|
return False
|
||||||
|
return any(_is_js_http_app(app_path) for app_path in apps.values())
|
||||||
|
|
||||||
|
|
||||||
|
def _update_http_apps_paths(
|
||||||
|
config_path: pathlib.Path, config: Config, local_deps: LocalDeps
|
||||||
|
) -> None:
|
||||||
|
"""Update HTTP apps paths to point to the correct location in the Docker container.
|
||||||
|
|
||||||
|
Similar to _update_http_app_path, but handles the `http.apps` dictionary
|
||||||
|
which maps path prefixes to app module paths. Python app paths are rewritten
|
||||||
|
to container paths; npm package references are left unchanged.
|
||||||
|
"""
|
||||||
|
if not (http_config := config.get("http")) or not (apps := http_config.get("apps")):
|
||||||
|
return
|
||||||
|
|
||||||
|
for prefix, app_str in apps.items():
|
||||||
|
module_str, _, attr_str = app_str.partition(":")
|
||||||
|
if not module_str or not attr_str:
|
||||||
|
message = (
|
||||||
|
'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
||||||
|
)
|
||||||
|
raise ValueError(message.format(import_str=app_str))
|
||||||
|
|
||||||
|
# Skip npm package references (bare module names without path separators)
|
||||||
|
if (
|
||||||
|
not module_str.startswith((".", "/"))
|
||||||
|
and not os.path.splitext(module_str)[1]
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "/" in module_str or "\\" in module_str:
|
||||||
|
resolved = (config_path.parent / module_str).resolve()
|
||||||
|
if not resolved.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Could not find HTTP app module for prefix '{prefix}': {resolved}"
|
||||||
|
)
|
||||||
|
elif not resolved.is_file():
|
||||||
|
raise IsADirectoryError(
|
||||||
|
f"HTTP app module must be a file for prefix '{prefix}': {resolved}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for path in local_deps.real_pkgs:
|
||||||
|
if resolved.is_relative_to(path):
|
||||||
|
container_path = (
|
||||||
|
pathlib.Path("/deps")
|
||||||
|
/ path.name
|
||||||
|
/ resolved.relative_to(path)
|
||||||
|
)
|
||||||
|
module_str = container_path.as_posix()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
for faux_pkg, (_, destpath) in local_deps.faux_pkgs.items():
|
||||||
|
if resolved.is_relative_to(faux_pkg):
|
||||||
|
container_subpath = resolved.relative_to(faux_pkg)
|
||||||
|
module_str = f"{destpath}/{container_subpath.as_posix()}"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"HTTP app module '{app_str}' for prefix '{prefix}' "
|
||||||
|
"not found in 'dependencies' list. "
|
||||||
|
"Add its containing package to 'dependencies' list."
|
||||||
|
)
|
||||||
|
apps[prefix] = f"{module_str}:{attr_str}"
|
||||||
|
|
||||||
|
|
||||||
def _get_node_pm_install_cmd(config_path: pathlib.Path, config: Config) -> str:
|
def _get_node_pm_install_cmd(config_path: pathlib.Path, config: Config) -> str:
|
||||||
def test_file(file_name):
|
def test_file(file_name):
|
||||||
full_path = config_path.parent / file_name
|
full_path = config_path.parent / file_name
|
||||||
@@ -957,6 +1057,8 @@ def python_config_to_docker(
|
|||||||
_update_checkpointer_path(config_path, config, local_deps)
|
_update_checkpointer_path(config_path, config, local_deps)
|
||||||
# Rewrite HTTP app path, so it points to the correct location in the Docker container
|
# Rewrite HTTP app path, so it points to the correct location in the Docker container
|
||||||
_update_http_app_path(config_path, config, local_deps)
|
_update_http_app_path(config_path, config, local_deps)
|
||||||
|
# Rewrite HTTP apps paths, so they point to the correct location in the Docker container
|
||||||
|
_update_http_apps_paths(config_path, config, local_deps)
|
||||||
|
|
||||||
pip_pkgs_str = (
|
pip_pkgs_str = (
|
||||||
f"RUN {local_reqs_pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
|
f"RUN {local_reqs_pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
|
||||||
@@ -1017,9 +1119,11 @@ ADD {relpath} /deps/{name}
|
|||||||
for fullpath, (relpath, name) in local_deps.real_pkgs.items()
|
for fullpath, (relpath, name) in local_deps.real_pkgs.items()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
has_js_apps = _has_js_http_apps(config)
|
||||||
install_node_str: str = (
|
install_node_str: str = (
|
||||||
"RUN /storage/install-node.sh"
|
"RUN /storage/install-node.sh"
|
||||||
if (config.get("ui") or config.get("node_version")) and local_deps.working_dir
|
if ((config.get("ui") or config.get("node_version")) and local_deps.working_dir)
|
||||||
|
or has_js_apps
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1078,6 +1182,10 @@ ADD {relpath} /deps/{name}
|
|||||||
"# -- End of JS dependencies install --",
|
"# -- End of JS dependencies install --",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
elif has_js_apps:
|
||||||
|
js_inst_str = (
|
||||||
|
f"ENV NODE_VERSION={config.get('node_version') or DEFAULT_NODE_VERSION}"
|
||||||
|
)
|
||||||
image_str = docker_tag(config, base_image, api_version)
|
image_str = docker_tag(config, base_image, api_version)
|
||||||
|
|
||||||
# Prepare docker file contents
|
# Prepare docker file contents
|
||||||
|
|||||||
@@ -446,6 +446,25 @@ class HttpConfig(TypedDict, total=False):
|
|||||||
Format: "path/to/module.py:app_var"
|
Format: "path/to/module.py:app_var"
|
||||||
If provided, it can override or extend the default routes.
|
If provided, it can override or extend the default routes.
|
||||||
"""
|
"""
|
||||||
|
apps: dict[str, str] | None
|
||||||
|
"""Optional. Record mapping path prefix to app module path.
|
||||||
|
|
||||||
|
Each key is a URL path prefix (e.g., "/dashboard"), and each value is an
|
||||||
|
import path in "file:export" format (e.g., "./dashboard.py:app").
|
||||||
|
|
||||||
|
Python apps (.py) are mounted in-process as FastAPI/Starlette sub-applications.
|
||||||
|
JS apps (.js, .ts, .mts, .mjs) are spawned as Node.js subprocess servers
|
||||||
|
with a reverse proxy forwarding requests from the main server.
|
||||||
|
npm package references (paths not starting with . or /) are resolved as
|
||||||
|
npm imports in the JS subprocess.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
{
|
||||||
|
"/dashboard": "./dashboard.py:app",
|
||||||
|
"/ext": "./extension.js:app",
|
||||||
|
"/conduit": "some-npm-package:app"
|
||||||
|
}
|
||||||
|
"""
|
||||||
disable_assistants: bool
|
disable_assistants: bool
|
||||||
"""Optional. If `True`, /assistants routes are removed from the server.
|
"""Optional. If `True`, /assistants routes are removed from the server.
|
||||||
|
|
||||||
|
|||||||
@@ -687,7 +687,7 @@
|
|||||||
},
|
},
|
||||||
"EncryptionConfig": {
|
"EncryptionConfig": {
|
||||||
"title": "EncryptionConfig",
|
"title": "EncryptionConfig",
|
||||||
"description": "Configuration for custom at-rest encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database,\nincluding metadata fields and checkpoint blobs.",
|
"description": "Configuration for custom at-rest encryption logic.\n\n Allows you to implement custom encryption for sensitive data stored in the database,\n including metadata fields and checkpoint blobs.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"path": {
|
"path": {
|
||||||
@@ -728,6 +728,20 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
|
"description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
|
||||||
},
|
},
|
||||||
|
"apps": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Optional. Record mapping path prefix to app module path.\n\nEach key is a URL path prefix (e.g., \"/dashboard\"), and each value is an\n\nPython apps (.py) are mounted in-process as FastAPI/Starlette sub-applications.\nJS apps (.js, .ts, .mts, .mjs) are spawned as Node.js subprocess servers\nwith a reverse proxy forwarding requests from the main server.\nnpm package references (paths not starting with . or /) are resolved as\nnpm imports in the JS subprocess.\n"
|
||||||
|
},
|
||||||
"configurable_headers": {
|
"configurable_headers": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -687,7 +687,7 @@
|
|||||||
},
|
},
|
||||||
"EncryptionConfig": {
|
"EncryptionConfig": {
|
||||||
"title": "EncryptionConfig",
|
"title": "EncryptionConfig",
|
||||||
"description": "Configuration for custom at-rest encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database,\nincluding metadata fields and checkpoint blobs.",
|
"description": "Configuration for custom at-rest encryption logic.\n\n Allows you to implement custom encryption for sensitive data stored in the database,\n including metadata fields and checkpoint blobs.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"path": {
|
"path": {
|
||||||
@@ -728,6 +728,20 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
|
"description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
|
||||||
},
|
},
|
||||||
|
"apps": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Optional. Record mapping path prefix to app module path.\n\nEach key is a URL path prefix (e.g., \"/dashboard\"), and each value is an\n\nPython apps (.py) are mounted in-process as FastAPI/Starlette sub-applications.\nJS apps (.js, .ts, .mts, .mjs) are spawned as Node.js subprocess servers\nwith a reverse proxy forwarding requests from the main server.\nnpm package references (paths not starting with . or /) are resolved as\nnpm imports in the JS subprocess.\n"
|
||||||
|
},
|
||||||
"configurable_headers": {
|
"configurable_headers": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import pytest
|
|||||||
from langgraph_cli.config import (
|
from langgraph_cli.config import (
|
||||||
_BUILD_TOOLS,
|
_BUILD_TOOLS,
|
||||||
_get_pip_cleanup_lines,
|
_get_pip_cleanup_lines,
|
||||||
|
_has_js_http_apps,
|
||||||
|
_is_js_http_app,
|
||||||
config_to_compose,
|
config_to_compose,
|
||||||
config_to_docker,
|
config_to_docker,
|
||||||
default_base_image,
|
default_base_image,
|
||||||
@@ -150,6 +152,148 @@ def test_validate_config():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_config_http_apps():
|
||||||
|
"""Test validation of http.apps field."""
|
||||||
|
# Valid http.apps config
|
||||||
|
config = validate_config(
|
||||||
|
{
|
||||||
|
"python_version": "3.12",
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": {"agent": "./agent.py:graph"},
|
||||||
|
"http": {
|
||||||
|
"apps": {
|
||||||
|
"/dashboard": "./dashboard.py:app",
|
||||||
|
"/ext": "./extension.js:app",
|
||||||
|
"/conduit": "some-npm-package:app",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert config["http"]["apps"] == {
|
||||||
|
"/dashboard": "./dashboard.py:app",
|
||||||
|
"/ext": "./extension.js:app",
|
||||||
|
"/conduit": "some-npm-package:app",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Valid with both app and apps
|
||||||
|
config = validate_config(
|
||||||
|
{
|
||||||
|
"python_version": "3.12",
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": {"agent": "./agent.py:graph"},
|
||||||
|
"http": {
|
||||||
|
"app": "./custom_routes.py:app",
|
||||||
|
"apps": {
|
||||||
|
"/dashboard": "./dashboard.py:app",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert config["http"]["app"] == "./custom_routes.py:app"
|
||||||
|
assert config["http"]["apps"] == {"/dashboard": "./dashboard.py:app"}
|
||||||
|
|
||||||
|
# Invalid prefix (doesn't start with /)
|
||||||
|
with pytest.raises(ValueError, match="Invalid http.apps prefix"):
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"python_version": "3.12",
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": {"agent": "./agent.py:graph"},
|
||||||
|
"http": {"apps": {"dashboard": "./dashboard.py:app"}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invalid app path (no colon separator)
|
||||||
|
with pytest.raises(ValueError, match="Invalid http.apps format"):
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"python_version": "3.12",
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": {"agent": "./agent.py:graph"},
|
||||||
|
"http": {"apps": {"/dashboard": "./dashboard.py"}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Empty apps dict is fine (no-op)
|
||||||
|
config = validate_config(
|
||||||
|
{
|
||||||
|
"python_version": "3.12",
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": {"agent": "./agent.py:graph"},
|
||||||
|
"http": {"apps": {}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert config["http"]["apps"] == {}
|
||||||
|
|
||||||
|
# None apps is fine
|
||||||
|
config = validate_config(
|
||||||
|
{
|
||||||
|
"python_version": "3.12",
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": {"agent": "./agent.py:graph"},
|
||||||
|
"http": {"apps": None},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert config["http"]["apps"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_js_http_app():
|
||||||
|
"""Test _is_js_http_app detection logic."""
|
||||||
|
# Python files are not JS
|
||||||
|
assert not _is_js_http_app("./dashboard.py:app")
|
||||||
|
assert not _is_js_http_app("./app.pyx:app")
|
||||||
|
|
||||||
|
# JS/TS files are JS
|
||||||
|
assert _is_js_http_app("./extension.js:app")
|
||||||
|
assert _is_js_http_app("./extension.ts:app")
|
||||||
|
assert _is_js_http_app("./extension.mts:app")
|
||||||
|
assert _is_js_http_app("./extension.mjs:app")
|
||||||
|
assert _is_js_http_app("./extension.cts:app")
|
||||||
|
assert _is_js_http_app("./extension.cjs:app")
|
||||||
|
|
||||||
|
# npm package references (bare module names) are JS
|
||||||
|
assert _is_js_http_app("some-npm-package:app")
|
||||||
|
assert _is_js_http_app("@scope/package:app")
|
||||||
|
|
||||||
|
# File paths without extensions that start with . or / are not JS
|
||||||
|
assert not _is_js_http_app("./mymodule:app")
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_js_http_apps():
|
||||||
|
"""Test _has_js_http_apps config detection."""
|
||||||
|
# No http config
|
||||||
|
assert not _has_js_http_apps({})
|
||||||
|
|
||||||
|
# No apps field
|
||||||
|
assert not _has_js_http_apps({"http": {}})
|
||||||
|
|
||||||
|
# Empty apps
|
||||||
|
assert not _has_js_http_apps({"http": {"apps": {}}})
|
||||||
|
|
||||||
|
# Python-only apps
|
||||||
|
assert not _has_js_http_apps(
|
||||||
|
{"http": {"apps": {"/dashboard": "./dashboard.py:app"}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# JS apps
|
||||||
|
assert _has_js_http_apps({"http": {"apps": {"/ext": "./extension.js:app"}}})
|
||||||
|
|
||||||
|
# npm package
|
||||||
|
assert _has_js_http_apps({"http": {"apps": {"/conduit": "some-npm-package:app"}}})
|
||||||
|
|
||||||
|
# Mixed
|
||||||
|
assert _has_js_http_apps(
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"apps": {
|
||||||
|
"/dashboard": "./dashboard.py:app",
|
||||||
|
"/ext": "./extension.js:app",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_config_image_distro():
|
def test_validate_config_image_distro():
|
||||||
"""Test validation of image_distro field."""
|
"""Test validation of image_distro field."""
|
||||||
# Valid image_distro values should work
|
# Valid image_distro values should work
|
||||||
@@ -491,6 +635,111 @@ WORKDIR /deps/outer-unit_tests/unit_tests\
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_docker_with_http_apps_python():
|
||||||
|
"""Test Docker generation with http.apps containing Python apps."""
|
||||||
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
|
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"dependencies": [".", "../../examples"],
|
||||||
|
"graphs": graphs,
|
||||||
|
"http": {
|
||||||
|
"apps": {
|
||||||
|
"/dashboard": "../../examples/my_app.py:app",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
base_image="langchain/langgraph-api",
|
||||||
|
)
|
||||||
|
# Python http.apps paths should be rewritten to container paths
|
||||||
|
assert "LANGGRAPH_HTTP" in actual_docker_stdin
|
||||||
|
assert "/deps/examples/my_app.py:app" in actual_docker_stdin
|
||||||
|
assert "/dashboard" in actual_docker_stdin
|
||||||
|
# No Node.js installation for Python-only apps
|
||||||
|
assert "install-node" not in actual_docker_stdin
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_docker_with_http_apps_js():
|
||||||
|
"""Test Docker generation with http.apps containing JS apps triggers Node install."""
|
||||||
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
|
# Use npm package references to avoid needing actual JS files on disk
|
||||||
|
actual_docker_stdin, _ = config_to_docker(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": graphs,
|
||||||
|
"http": {
|
||||||
|
"apps": {
|
||||||
|
"/ext": "my-js-package:app",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
base_image="langchain/langgraph-api",
|
||||||
|
)
|
||||||
|
# JS http.apps should trigger Node.js installation
|
||||||
|
assert "install-node" in actual_docker_stdin
|
||||||
|
assert "NODE_VERSION=20" in actual_docker_stdin
|
||||||
|
assert "LANGGRAPH_HTTP" in actual_docker_stdin
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_docker_with_http_apps_npm_package():
|
||||||
|
"""Test Docker generation with http.apps containing npm package triggers Node install."""
|
||||||
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
|
actual_docker_stdin, _ = config_to_docker(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": graphs,
|
||||||
|
"http": {
|
||||||
|
"apps": {
|
||||||
|
"/conduit": "some-npm-package:app",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
base_image="langchain/langgraph-api",
|
||||||
|
)
|
||||||
|
# npm package should trigger Node.js installation
|
||||||
|
assert "install-node" in actual_docker_stdin
|
||||||
|
assert "NODE_VERSION=20" in actual_docker_stdin
|
||||||
|
assert "LANGGRAPH_HTTP" in actual_docker_stdin
|
||||||
|
# npm package path should not be rewritten
|
||||||
|
assert "some-npm-package:app" in actual_docker_stdin
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_to_docker_with_http_apps_mixed():
|
||||||
|
"""Test Docker generation with mixed Python and JS http.apps."""
|
||||||
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
|
actual_docker_stdin, _ = config_to_docker(
|
||||||
|
PATH_TO_CONFIG,
|
||||||
|
validate_config(
|
||||||
|
{
|
||||||
|
"dependencies": [".", "../../examples"],
|
||||||
|
"graphs": graphs,
|
||||||
|
"http": {
|
||||||
|
"app": "../../examples/my_app.py:app",
|
||||||
|
"apps": {
|
||||||
|
"/dashboard": "../../examples/my_app.py:app",
|
||||||
|
"/conduit": "some-npm-package:app",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
base_image="langchain/langgraph-api",
|
||||||
|
)
|
||||||
|
# Should have both Python path rewriting and Node.js installation
|
||||||
|
assert "install-node" in actual_docker_stdin
|
||||||
|
assert "NODE_VERSION=20" in actual_docker_stdin
|
||||||
|
assert "LANGGRAPH_HTTP" in actual_docker_stdin
|
||||||
|
# Python paths should be rewritten
|
||||||
|
assert "/deps/examples/my_app.py:app" in actual_docker_stdin
|
||||||
|
|
||||||
|
|
||||||
def test_config_to_docker_outside_path():
|
def test_config_to_docker_outside_path():
|
||||||
graphs = {"agent": "./agent.py:graph"}
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||||
|
|||||||
Reference in New Issue
Block a user